SwadhyAI Notes

All Low-Level Design notes

GRASP · Cohesion and coupling

Low coupling, high cohesion

Cohesion asks whether a class's methods belong together. Coupling asks how many other classes must stay still when you edit one. You change hourly pricing and six classes stop compiling — that is high coupling: too many places knew the formula. You open Ticket and it also sends email and writes a CSV — that is low cohesion: those methods do not belong together. Both make the next requirement expensive.

Cohesion

Cohesion measures whether the methods of a class serve one clear job. High cohesion: Ticket tracks identity, time bounds, and status — everything about one stay. Low cohesion: Ticket also formats SMS and talks to Stripe. A useful test: if you need a long detour to explain why a method lives here, it probably belongs elsewhere.

Coupling

Coupling measures how much one class knows about others. High coupling: a controller that reaches ticket.getSpot().getFloor().getLot().getClock(). Low coupling: ticket.duration(clock) and a lot that exposes findSpot(criteria). When coupling is low, a change stays local instead of rippling across the graph.

What must change together?

Measure a design by what must change together. If pricing and floor layout always move as a pair, your types are lying about being separate. If they never should move together, they must not share a class or a long getter chain.

GRASP's "low coupling, high cohesion" is the same idea you feel when a file becomes scary to open. Single Responsibility is cohesion with an eye on reasons to change. When you extract PricingStrategy you raise cohesion in ParkingLot and lower coupling to the current fee formula. The Law of Demeter is coupling you can hear: each extra get is another class that must stay still for you to compile.

A picture of both axes

High cohesion, low coupling

  Ticket            stay: id, times, status
  PricingStrategy   fee policy
  ParkingLot        floors, allocation strategy

Low cohesion     Ticket.sms() + Ticket.csv() + Ticket.fee()
High coupling    fee() inlined in Lot, Floor, Gate, Report, Refund, SMS

Java: a change that used to touch six classes

Every caller that inlines hours × rate is coupled to the formula. GST changes, and ExitService, DailyCsv, RefundHelper, the SMS template, and a test fixture all reopen. After you extract pricing, they ask pricing.fee — one home for the rule. A cohesive Ticket still answers parked(); it does not answer tax.

java
// Coupled: every caller knows the formula (and each risks truncating partial hours)
Money fee = hourly.times(ticket.getExit().minus(ticket.getEnter()));
long seconds = Duration.between(t.getEnter(), t.getExit()).getSeconds();
if (seconds < 0) throw new IllegalArgumentException("negative stay");
long hours = Math.max(1, seconds / 3600 + (seconds % 3600 == 0 ? 0 : 1));
Money report = hourly.times(hours);
Money refund = hourly.times(t.getHours());

// Cohesive Ticket, decoupled callers
Money fee = pricing.fee(ticket, clock);

class Ticket {
  Duration parked(Clock clock) {
    return Duration.between(enteredAt, clock.instant());
  }
}

Decoupling by adding a middle man that knows everyone

A ParkingMediator that reaches into Lot, Floor, Spot, Ticket, and Stripe did not lower coupling. It hid the coupling in one file. The rest of the system looks clean while the mediator becomes the six-class compile break by itself. A real reduction in coupling means fewer foreign names in each file — not one file that imported all the names.

java
class ParkingMediator {
  Money checkout(TicketId id) {
    Ticket t = lot.findTicket(id);
    Floor f = t.getSpot().getFloor();
    RateCard rates = f.getLot().getRateCard();
    Money due = rates.hourly().times(t.hours());
    stripe.charge(t.getVehicle().getOwner().getCard(), due);
    twilio.send(t.getVehicle().getOwner().getPhone(), due.toString());
    return due;
  }
}
// Every type in the graph must stay still for this method to compile.

Splitting a class that was already one job

Splitting Ticket into TicketTimes, TicketStatus, and TicketId can raise coupling without adding meaning. Exit now talks to three objects that always change together. Split on reasons to change, not on field count. Identity, times, and status of a stay are one cohesive clump. SMS is not.

How to explain this in an interview

Point at a file and say what would force it open. If two different people would edit it for unrelated reasons, cohesion is low. Point at a method and count foreign types. If the count includes Floor, RateCard, Stripe, and Phone, coupling is high. Then name the extract: PricingStrategy, Notifier, Ticket.parked(clock).

High cohesion is not "one method per class." A Floor that adds and finds spots is cohesive. A Floor that also prices tickets is not. Low coupling is not "no dependencies." ParkVehicle should depend on ParkingLot and TicketRepository. It should not depend on TwilioClient.

Fees, tickets, and the compile blast radius

After a healthy split, changing GST recompiles HourlyWithGst and tests for that class. Changing nearest-spot allocation recompiles NearestSpotStrategy. Changing SMS recompiles TwilioNotifier. The lot, the ticket, and the HTTP layer stay still. That blast radius is the measurement. If a one-line tax edit greps across six packages, coupling won.

java
class HourlyWithGst implements PricingStrategy {
  private final GstRate gst;
  public Money fee(Ticket ticket, Clock clock) {
    Money base = ticket.hourlyRate().times(ticket.hoursParked(clock));
    return base.plus(base.times(gst.current()));
  }
}
// Finance changes gst.current() or this class.
// Floor.java does not compile-break.

A pricing edit that used to fan out

Imagine GST moving from 18% to 18% plus a municipal cess. In the coupled design you grep for 0.18 and find it in ExitService, DailyCsv, RefundHelper, a Twilio string builder, a test fixture, and an admin "preview fee" endpoint. Six files, six chances to miss. In the cohesive design you open HourlyWithGst, change gst.current(), and run the pricing tests. The report still calls pricing.fee. The SMS still prints the Money it was handed. The blast radius is the point of this page.

java
class DailyCsv {
  private final PricingStrategy pricing;
  String render(List<Ticket> closed, Clock clock) {
    StringBuilder out = new StringBuilder("id,fee\n");
    for (Ticket t : closed) {
      out.append(t.id()).append(',').append(pricing.fee(t, clock)).append('\n');
    }
    return out.toString();
  }
}

class TwilioNotifier implements Notifier {
  public void ticketClosed(Ticket ticket, Money due) {
    client.send(ticket.phone(), "Pay " + due);
  }
}
// Neither class knows 0.18. Neither class imports Floor.

Cohesion inside a strategy

A PricingStrategy that also allocates spots is low cohesion wearing an interface. The abstraction was named for fees; do not hide allocation behind it. A TicketRepository that also sends email is the same failure at the edge. Name the type after the job, then refuse methods that belong to a different reason to change.

High coupling can hide inside a cohesive class too. A Ticket that stores a live ParkingSpot reference will drag Floor and Lot into every test that constructs a stay. Prefer a SpotId value on the ticket. The lot remains the place that maps ids to spots. That cut lowers coupling without splitting the stay into meaningless pieces.

Related

Say the blast radius out loud

Interviewer

GST just changed. Which files do you open?

You

HourlyWithGst and its tests. Ticket still answers parked(). DailyCsv still calls pricing.fee. Twilio still prints the Money it was handed. Floor does not compile-break.

Interviewer

What if I also want nearest-spot allocation this week?

You

That is a second reason to change. NearestSpotStrategy is a new class. Pricing does not reopen. Two reasons to change, two classes — that is high cohesion. The lot depends on two strategy interfaces, so coupling stays low.

Interviewer

Could I keep one ParkingHelper for both?

You

That helper would reopen for tax and for walking the floor. It looks decoupled from the UI and is still a knot. I would rather two small types than one class that knows everyone.

Ticket should not hold a live Spot

A Ticket field of type ParkingSpot couples every stay test to floors, chargers, and occupy(). Store SpotId. Occupancy remains on the spot. History remains on the ticket. That single type change drops coupling without hurting cohesion: the stay is still one job, it just no longer drags the building into every assertion.

java
class Ticket {
  private final TicketId id;
  private final SpotId spotId;
  private final Instant enteredAt;
  SpotId spotId() { return spotId; }
}

class ParkingLot {
  ParkingSpot spot(SpotId id) {
    return spots.stream().filter(s -> s.id().equals(id)).findFirst().orElseThrow();
  }
}

Count foreign types in a method

Open checkout and list every type it names besides its own. Ticket, PricingStrategy, TicketRepository, Clock, Money is a healthy list. Floor, RateCard, StripeClient, Owner, Phone is a coupled list. That count is a review trick you can do on a whiteboard without a metrics tool. High cohesion is the other check: would you explain this method in the same sentence as the class name? ExitService.checkout yes. Ticket.sendSms no.

When both checks fail, extract. When only coupling fails, you probably walked a getter chain. When only cohesion fails, you probably piled a second job onto an otherwise focused type. The two axes diagnose different edits.

A change that should be local

Municipal cess on GST is a pricing change. Nearest-to-elevator is an allocation change. Twilio template wording is a notifier change. If those three sentences reopen the same file, cohesion is lying. If they reopen six files each, coupling is lying. The healthy design makes each sentence a one-file edit plus tests. That is the only metric this page asks you to remember in an interview.

Cohesion is a sentence; coupling is a guest list

"Ticket tracks a stay" is one sentence. "Ticket tracks a stay and texts Twilio and writes CSV" is three. That is cohesion. "checkout names Ticket, PricingStrategy, Clock" is a short guest list. "checkout names Floor, RateCard, Stripe, Owner" is a party. Keep the sentence short and the guest list short. When either grows, extract before the next follow-up lands. A mediator that invites everyone to the same party did not shorten the list — it moved the party into one room with a nicer name.

Use that two-check review on every God object the interviewer sketches. If the sentence has "and," split. If the guest list has types that live three packages away, hide them behind a strategy or abstraction. You do not need a coupling metric library in a two-hour interview — you need those two questions on the whiteboard next to the class list. GST, nearest-spot, and SMS wording should each map to a different file when you point at the board. That mapping is the design.

Takeaway

Cohesion asks whether the methods belong together. Coupling asks how many other classes must stay still when you edit. Measure by blast radius: GST should reopen the pricing class, not Floor, Gate, and SMS. Do not "decouple" by inventing a mediator that knows everyone, and do not split a stay into three types that always move as one.