SwadhyAI Notes

All Low-Level Design notes

SOLID · S

Single Responsibility

Single Responsibility says a class should have one reason to change. “Reason to change” means one person or one policy that would force an edit — not “one method” and not “a short file.”

In a parking lot, that idea shows up clearly. If ParkingLot parks cars, computes fees, writes SQL, and sends SMS, then tax people, DBAs, and the notifications team all need to edit the same file. Monday the GST rule changes. Tuesday the SMS vendor changes. Wednesday you cannot tell which edit broke allocation. The class had too many reasons to change.

Count reasons, not methods

Ask: if this requirement changes, how many independent reasons would force an edit to this class? That is the question. A God object appears when leftover verbs land in one Manager. ParkingService.park, .price, .persist, .notify looks tidy on a whiteboard and becomes a knot in code. Split along the axes of change: allocation, pricing, persistence, notification.

One reason is not one method. Ticket may hold identity, time bounds, and status — those change together when the parking stay changes. That is still one job. Ticket.sendSms() is a second job. Pull it out. Splitting Ticket into TicketId, TicketTimes, and TicketStatus as three types can raise coupling without raising meaning. Split on reasons to change, not on field count.

When you extract PricingStrategy you raise cohesion in ParkingLot and you give tax a place to live. SRP and high cohesion are the same instinct with different names. SOLID’s S is cohesion with an eye on who would need to edit the file tomorrow.

One class, three days, three owners

The week the God object failed

  1. Monday

    GST on parking fees

    Tax people

    You open ParkingLot and hunt through park(), fee(), save(), and sms() to find the rate math.

  2. Tuesday

    SMS vendor swap

    Notifications team

    You open the same class. A typo in the Twilio call silently breaks allocation tests that construct ParkingLot.

  3. Wednesday

    Nearest-spot rule

    Ops / product

    You cannot tell which of Monday’s or Tuesday’s edits broke the floor walk. The class had too many reasons to change.

Java: four reasons glued together

Count the people who would need to edit this file. Ops changes how spots are chosen. Finance changes GST. Persistence changes the INSERT. Comms changes Twilio. Four different reasons to change, one class. That is the smell.

java
class ParkingLot {
  private final Connection jdbc;
  private final TwilioClient twilio;
  private final List<ParkingSpot> spots;

  Spot park(Vehicle v) {
    ParkingSpot spot = spots.stream()
        .filter(ParkingSpot::isFree)
        .filter(s -> s.fits(v))
        .findFirst()
        .orElseThrow();
    spot.occupy(v);
    return spot;
  }

  Money fee(Ticket t) {
    long seconds = Duration.between(t.enteredAt(), Instant.now()).getSeconds();
    if (seconds < 0) throw new IllegalArgumentException("negative stay");
    long hours = Math.max(1, seconds / 3600 + (seconds % 3600 == 0 ? 0 : 1));
    Money base = t.hourlyRate().times(hours);
    return base.plus(base.times(0.18)); // GST lives here, for now
  }

  void save(Ticket t) {
    jdbc.execute("INSERT INTO tickets (id, vehicle, entered) VALUES (?, ?, ?)");
  }

  void sms(Ticket t) {
    twilio.send(t.phone(), "Your ticket " + t.id() + " is open");
  }
}

Split along reasons to change

After the split, ParkingLot still coordinates a stay. It does not own tax, SQL, or SMS. Each collaborator can change without dragging the others through a compile. Tests construct a lot with a fake allocator and never touch Twilio.

java
class ParkingLot {
  private final SpotAllocationStrategy allocation;
  private final List<ParkingSpot> spots;

  ParkingLot(SpotAllocationStrategy allocation, List<ParkingSpot> spots) {
    this.allocation = allocation;
    this.spots = List.copyOf(spots);
  }

  Optional<ParkingSpot> park(Vehicle v) {
    return allocation.choose(freeSpots(), v);
  }

  private List<ParkingSpot> freeSpots() {
    return spots.stream().filter(ParkingSpot::isFree).toList();
  }
}

interface PricingStrategy { Money fee(Ticket t, Clock clock); }
interface TicketRepository { void save(Ticket t); Optional<Ticket> find(TicketId id); }
interface Notifier { void ticketOpened(Ticket t); void ticketClosed(Ticket t); }

class HourlyWithGst implements PricingStrategy {
  public Money fee(Ticket t, Clock clock) {
    Money base = t.hourlyRate().times(t.hoursParked(clock));
    return base.plus(base.times(new GstRate().current()));
  }
}

One reason is not one method

Beginners hear “single responsibility” and explode Ticket into a dozen one-line types. That is not the habit. A stay is one job: identity, entry, exit, status. Those fields change together when the driver leaves. Keep them on Ticket.

The second job is the one that answers to a different person. Ticket.formatSms() answers to comms. Ticket.toCsvRow() answers to reporting. Ticket.insertSql() answers to the DBA. Those are extra reasons to change. They do not belong on the stay.

A thin controller may call park, then save, then notify. That is orchestration of one use case, not four policies living in one domain class. If the controller starts computing GST, the expert was skipped and SRP is already failing on the application edge.

Keep together

Ticket id, vehicle, spot id, enteredAt, exitedAt, status. One stay. One reason: the parking visit changed.

Pull out

GST math, JDBC, Twilio, CSV export, nearest-spot walking. Each answers to a different reason to change.

How to explain this in an interview

Name the verbs on the whiteboard. Park. Price. Persist. Notify. Ask who would request a change to each verb. If two verbs share an owner, they may share a class. If they do not, they must not. That conversation is the design.

Then draw the collaborators. ParkingLot keeps floors and asks an allocation strategy. An exit use case asks Ticket for duration and PricingStrategy for money. TicketRepository sits at the edge. Notifier sits at the edge. You have not added premature abstractions. You have split the design so each reason to change has its own home.

VerbWho changes itHome after the split
Allocate spotOps / product (nearest, EV preferred)SpotAllocationStrategy
Compute feeFinance / taxPricingStrategy
Record ticketPersistence / DBATicketRepository
Text the driverComms / vendorNotifier
Track the stayThe parking visit itselfTicket

Renaming the God object does not count

ParkingFacade that still prices, persists, and notifies is the same class with a prettier name. Count reasons to change, not syllables. ParkingHelper, ParkingUtils, and ParkingManager fail the same test.

A facade can be honest if it only forwards: receive the system operation, ask the experts, return. That is a GRASP controller, and it stays thin. The moment GST math lands in the facade, you are back to Monday’s file.

What a unit test reveals

If constructing ParkingLot in a test requires a JDBC connection and a Twilio client, the class already has too many reasons to exist. A focused test should park a car with an in-memory list of spots and never mention SMS. A pricing test should feed a ticket and a clock and never mention floors. When those tests cannot be written separately, SRP is not a slogan yet — it is still one file.

Related habits

Takeaway

A class should have one reason to change — one person or policy that would force an edit. Ticket can hold a whole stay. Ticket must not also send SMS and write SQL. Split the God object along axes of change, not along method count, and do not celebrate a rename that left all four jobs in one file.