SwadhyAI Notes

All Low-Level Design notes

Design Principles

SOLID as five habits

SOLID is a set of five design habits that keep change cheap. When they are missing, one new requirement reopens many existing files. In a parking-lot design, adding compact SUVs might force edits to ParkingLot, Floor, an allocation switch, a pricing switch, and a serializer. With SOLID, that same requirement should usually become a new class or strategy instead of a five-file rewrite.

Learn them as full sentences, not as five letters. A class should have one reason to change. Extend by adding, not by rewriting. A subtype must honor its parent’s contract. Keep interfaces small. Depend on an abstraction you own. In a machine-coding interview, pointing at a class and naming the habit it violates is more useful than reciting the acronym and stopping there.

Five sentences

  1. 1

    S — A class should have one reason to change.

    Single Responsibility

  2. 2

    O — Extend behavior by adding code, not by rewriting existing modules.

    Open-Closed

  3. 3

    L — A subtype must honor the contract of its parent type.

    Liskov Substitution

  4. 4

    I — Prefer small interfaces so callers depend only on what they use.

    Interface Segregation

  5. 5

    D — Depend on an abstraction you own, not on a concrete database driver.

    Dependency Inversion

What will change, and how many classes must you edit?

SOLID is five habits for keeping a design cheap to change. The interviewer’s follow-up is almost always a change: a new vehicle type, a new fee rule, a new payment vendor, or a new report. A design follows SOLID when that follow-up lands in one new type, not in five existing files. If you cannot name what will change, you cannot protect the design against that change.

The parking-lot round makes the idea concrete. Pricing will change. Allocation will change. Persistence might move from an in-memory map to JDBC. Notification might move from SMS to email. Those are four separate axes of change. A ParkingManager that owns all four works against these habits. Split along the axes, then put the unstable ones behind abstractions you own.

SOLID does not tell you who calculates the fee. That is GRASP — the question of where a responsibility should live. Use both. GRASP places the responsibility. SOLID keeps that placement from rotting when the next vehicle type arrives. A class that follows Single Responsibility, such as a PricingHelper, can still be the wrong object if Ticket already had the times and a pricing strategy already had the policy.

Compact SUV follow-up

Why SOLID matters on a parking lot

Start with a clear definition of the problem: one new vehicle type should not reopen every module that touches vehicles. In the first version you support cars and bikes with a switch on VehicleType inside allocation, another inside pricing, another inside the JSON serializer, plus a size check on Floor. It works. Then the interviewer asks for compact SUVs, then staff vehicles, then EVs that prefer chargers. Each type reopens the same five files. Tests churn. The switches grow. That repeated reopen is the smell SOLID is meant to remove.

edit ParkingLot
edit Floor
edit allocation switch
edit pricing switch
edit serializer

After the habits are in place, a new vehicle type is a new class that answers “can this spot take me?” and a pricing strategy that answers “what do I cost?” The lot, the floor, and the serializer stay closed. That is the whole point of applying SOLID here.

Walk the five habits on one lot

S

Single Responsibility

A class should have one reason to change.

If allocation policy changes, you should not also have to edit persistence code.

O

Open-Closed

Extend behavior by adding code, not by rewriting existing modules.

A new vehicle type should be a new class or strategy, not another case in five switches.

L

Liskov Substitution

A subtype must honor the contract of its parent type.

If callers write if (spot instanceof HandicappedSpot), the parent contract already leaked.

I

Interface Segregation

Prefer small interfaces so callers depend only on what they use.

Do not force a read-only report to implement park().

D

Dependency Inversion

Depend on an abstraction you own, not on a concrete database driver.

The domain talks to TicketRepository, not to Postgres.

Each habit has its own page because the failure mode is different. Single Responsibility fails as a God object. Open-Closed fails as a growing switch. Liskov fails as a lying subtype. Interface Segregation fails as a fat interface. Dependency Inversion fails when the domain imports JDBC. Name the failure, then name the habit.

What SOLID is not

Not a checklist you dump at minute one. Saying “I will apply SOLID” is not a design. Say which habit applies, on which class, and what the next requirement would cost without it.

Not a reason to skip GRASP. Placement comes first. A class with one reason to change can still be the wrong home for the job. Ask who has the data, who should create the ticket, and what will vary. Then keep that placement cheap to change.

Not a license for premature abstraction. Open-Closed and Dependency Inversion apply where a second variant is likely. Ticket id format probably will not vary. Pricing will. Do not invent a TicketIdFactory as an unnecessary interface when nothing is going to vary there.

How to explain this in an interview

Point at a class: “This has two reasons to change, so I would split it.” Point at a switch: “This will grow, so I would put it behind a strategy interface.” Point at new JdbcTicketStore() inside the service: “The domain should depend on TicketRepository.” That is SOLID as a habit you can apply under pressure, not as a slogan.

Java: the lot that reopened for every type

The before sketch is what you get when every habit is skipped at once. Allocation, pricing, persistence, and notification share one class. Vehicle types live in switches. Tests need a live database. Adding compact SUVs touches all of it.

java
class ParkingLot {
  Spot park(Vehicle v) {
    switch (v.type()) {
      case CAR -> findFirst(SpotType.COMPACT);
      case BIKE -> findFirst(SpotType.MOTORCYCLE);
      default -> throw new IllegalArgumentException("unknown type");
    }
  }

  Money fee(Ticket t) {
    return switch (t.vehicle().type()) {
      case CAR -> hourly.times(t.hours());
      case BIKE -> hourly.times(t.hours()).times(0.5);
      default -> throw new IllegalArgumentException("unknown type");
    };
  }

  void save(Ticket t) { jdbc.execute("INSERT INTO tickets ..."); }
  void sms(Ticket t) { twilio.send(t.phone()); }
}

The after sketch is not “more classes for sport.” It is one home per reason to change, a strategy interface in front of the rules that will move, and a repository abstraction the domain owns. Compact SUVs become a vehicle that answers size, plus a pricing implementation if the rate is different. The lot file stays closed.

java
class ParkingLot {
  private final SpotAllocationStrategy allocation;
  private final PricingStrategy pricing;
  private final TicketRepository tickets;

  ParkingLot(SpotAllocationStrategy allocation,
             PricingStrategy pricing,
             TicketRepository tickets) {
    this.allocation = allocation;
    this.pricing = pricing;
    this.tickets = tickets;
  }

  Ticket enter(Vehicle vehicle, Clock clock) {
    ParkingSpot spot = allocation.choose(freeSpots(), vehicle)
        .orElseThrow(() -> new IllegalStateException("lot full"));
    Ticket ticket = new Ticket(ids.next(), vehicle, spot.id(), clock.instant());
    spot.occupy(vehicle);
    tickets.save(ticket);
    return ticket;
  }

  Money feeFor(Ticket ticket, Clock clock) {
    return pricing.fee(ticket, clock);
  }
}

interface SpotAllocationStrategy {
  Optional<ParkingSpot> choose(List<ParkingSpot> free, Vehicle vehicle);
}

interface PricingStrategy {
  Money fee(Ticket ticket, Clock clock);
}

interface TicketRepository {
  void save(Ticket ticket);
  Optional<Ticket> find(TicketId id);
}

How the five habits show up in that sketch

S — Single Responsibility. A pricing change no longer forces you into the same file as SQL. Notification is not even on the lot. Each collaborator has one reason to open.

O — Open-Closed. ParkingLot.enter does not switch on vehicle type. A new allocation rule is a new SpotAllocationStrategy. The lot stays closed.

L — Liskov Substitution. Every strategy honors the same method contract. An implementation that throws for bikes is only legal if the interface already said it may return empty. A no-op that pretends to park would break substitution.

I — Interface Segregation. The lot depends on TicketRepository, not on an oversized ParkingOps interface that also exports CSV. A daily report can implement a tiny LotReport without pretending to park cars.

D — Dependency Inversion. The lot imports TicketRepository, an interface owned by the use case. JDBC lives in an adapter. Tests pass an in-memory map. The direction of source-code imports is the design.

A short round: name the habit, not the letters

Interviewers grade whether you can attach a habit to a class under pressure. This exchange is the pacing you want: scope the change, point at the smell, then name the abstraction that isolates it.

Interviewer

Add compact SUVs. They need larger spots and a different hourly rate.

You

Today allocation and pricing both switch on VehicleType, so this follow-up would reopen ParkingLot twice. I would put allocation behind SpotAllocationStrategy and pricing behind PricingStrategy so a new type is a new implementation.

Interviewer

Where does the fee formula live?

You

Ticket knows entry and exit. The strategy knows the rate card and tax. A thin exit use case asks both. That is GRASP placement; SOLID keeps those two objects from growing a third job.

Interviewer

We also need a CSV of today’s tickets. Should ParkingLot grow an export method?

You

No. That is a second reason to change and a fat interface waiting to happen. A LotReport port that reads tickets is enough. The lot stays about parking.

Two mistakes that waste the round

Reciting the letters. Five acronyms on the whiteboard with no class next to them do not move the design. The interviewer already knows the names. They want to see which file you would refuse to reopen.

Using SOLID to skip GRASP. You can split ParkingManager into five helpers and still put the fee on the wrong object. First ask who has the information. Then keep that object from growing a second job. See Information Expert and cohesion and coupling.

Where to go next

Walk the five habits in order if you are teaching yourself. Jump to GRASP if the round is about placement. Jump to the parking-lot case when you want the habits inside a full machine-coding sketch.

Takeaway

SOLID is five sentences you apply to named classes. A class should have one reason to change. Add a type instead of reopening a switch. Honor the parent’s contract. Keep interfaces small. Depend on an abstraction you own. GRASP places the job; these habits keep that placement cheap when compact SUVs arrive.