SwadhyAI Notes

All Low-Level Design notes

Production LLD

Refactoring toward a better design

You start a parking-lot interview and draw ParkingLotManager, a first-cut class that parks, prices, and texts. That is allowed. The interviewer is not grading a memorized finished diagram. They want to see whether you can move a method to the (the type that already has the data) while the tests stay green.

means improving structure without changing behavior, usually in small, tested steps. It is the loop on the right, not a confession that you started wrong.

Draft to delete

1

Red / working draft

Get park and exit working. A ParkingLotManager blob is allowed on the first cut.

2

Net

Two tests: happy path and lot full. Clock frozen so fees stay stable.

3

Extract

PricingStrategy when the fee ifs hurt. Pull the repository out of the manager.

4

Move

Move the method toward the data. Rename Manager to a coordinating controller.

5

Delete

Dead leftover after a move is not a backup. Version control is.

What actually happens in the interview

The first class list is a draft. Get park and exit working, even if one manager owns every verb. Then you improve structure: peel a policy, move a method toward its data, delete the leftover. Behavior (the fee, the full-lot rejection) must not change.

That only works with a . Two tests (happy path and lot full) plus a frozen Clock (injected time so "now" does not drift) let you without holding the whole design in your head. Talking about a refactor you never start is weaker than one interface that already exists.

Feature envy (a method more interested in another object's fields than its own) is the usual signal. If ParkingLot reads Ticket getters to build a fee, the formula belongs on a policy or on Ticket. The sections below show the same numbers in a new home, then the extract order that keeps the race you already solved.

Simple rule of thumb

The draft-and-net story above decides how you ship the next edit. Once park and exit work, do not open a blank file and redraw. Change structure; keep behavior. Use the already on the page so each is small enough to narrate.

Rule of thumb

One extract, one rename, one delete, each spoken out loud. A silent ten-minute rewrite looks like panic. Interviewers like hearing "I would extract this next," then seeing you do a small extract.

The three moves after the draft works

The rule of thumb says narrate one move at a time. Here is what extract, move, and delete look like on the manager you already have, without throwing away a working park().

Extract in place

the fee policy when the ifs hurt, then split the repository out of the manager.

Rename Manager

When it only coordinates, it is a controller. ParkingService.park delegates; the God object (one class with every verb) shrinks by losing verbs, not comments.

Delete leftover

Dead code is not a backup after the method moved. Version control keeps history; the old branch can go.

From a blob to a policy, behavior unchanged

Those three moves leave the fee numbers where they were. Only the home changes. Clock stays injected so the extract does not sneak LocalDateTime.now() back in (the playbook below shows the HourlyPricing shape that keeps Instant a parameter).

java
// before: fee logic inside ParkingLot.exit: feature envy + primitive obsession
Money fee = hours * (vehicle instanceof EvCar ? 20 : 10);

// after: same numbers, different home
Money fee = pricing.fee(ticket, clock.instant());

class ParkingLot {
  Receipt exit(TicketId id) {
    Instant now = clock.instant();
    Ticket ticket = tickets.require(id);
    Money fee = pricing.fee(ticket, now);
    ticket.close(now);
    tickets.save(ticket);
    return new Receipt(ticket.id(), fee, now);
  }
}

Move method toward the data

The before snippet is feature envy: ParkingLot is more interested in Ticket's fields than its own. The after line hands the same work to the or a policy that already holds the values.

java
// feature envy: ParkingLot is more interested in Ticket's data than its own
long seconds = Duration.between(ticket.getEntry(), clock.instant()).getSeconds();
if (seconds < 0) throw new IllegalArgumentException("negative stay");
int hours = (int) Math.max(1, seconds / 3600 + (seconds % 3600 == 0 ? 0 : 1));
double raw = hours * ticket.getRate();
if (ticket.getVehicleType().equals("EV")) raw *= 1.2;

// move toward the expert / policy
Money fee = pricing.fee(ticket, clock.instant());

What to extract first

Once you can see envy in the fee code, use this table to pick the smallest next type. Shotgun surgery means one change hits many classes; OCP (Open-Closed Principle) says a new vehicle type should not edit a switch. Primitive obsession is a raw double or String where Money or Email should exist. A TicketRepository port hides how tickets are stored, so exit() does not construct Postgres.

You feelMoveSmell named
Fee ifs growingPricingStrategyShotgun surgery / OCP
Lot talks to Ticket fieldsPolicy or Ticket methodFeature envy
double amount, String emailMoney, EmailPrimitive obsession
Manager parks, prices, SMSSplit types + thin controllerGod object
new Postgres in exit()TicketRepository portHidden dependency

A worked extract: Manager to service

The table says where to go. This is the order that gets you there from a ParkingLotManager.park that also prices, saves, and texts. After the in the hero loop is green, peel one verb per step. Do not peel all four in one edit; that is a rewrite wearing extract clothing.

Step A

Inject Clock

Put clock.instant() at the top of exit. Fees become deterministic before any strategy exists.

Step B

Extract PricingStrategy

Move the EV versus car ifs. ParkingLot.exit still coordinates. Tests assert the same Money.

Step C

Extract TicketRepository

HashMap fake in tests, Postgres later. Persistence stops being a reason that one class must change.

Step D

Rename to ParkingService

It now only orchestrates. Allocation and notify follow if time remains. Delete empty Manager leftovers.

Keep Clock injected while you extract

Step A made fees deterministic. The common regression is that the strategy extract puts LocalDateTime.now() back inside HourlyPricing. Then the fee test you used as a starts failing at midnight. Pass Instant (the timestamp argument) from the service. The must not hide a new dependency.

java
// during extract: Instant stays a parameter
class HourlyPricing implements PricingStrategy {
  public Money fee(Ticket ticket, Instant exitAt) {
    Duration stay = Duration.between(ticket.enteredAt(), exitAt);
    long seconds = stay.getSeconds();
    if (seconds < 0) throw new IllegalArgumentException("negative stay");
    long hours = Math.max(1, seconds / 3600 + (seconds % 3600 == 0 ? 0 : 1));
    return ticket.tariff().times(hours);
  }
}

Feature envy check after each step

If exit still reads four Ticket getters to build a fee, the is incomplete. The policy should take Ticket and Instant, or Ticket should ask the policy. Primitive obsession (double hours * 10) should die in the same move as the strategy extract. Money belongs in the policy return type.

What still goes wrong, and how to say it

You now have the loop, the three moves, and a four-step extract. These are the ways still fails in a forty-five minute round, plus the sentence that scores when you start one.

Common mistakes

Rewrite from scratch mid-interview

You had a working park(). A greenfield second design often misses the race you already solved.

Refactoring without a test

You will drop the full-lot case. One assertion keeps the behavior you already have.

Big-bang rename of every class

Skip the narrated one-move habit above; a silent rewrite of every name looks like panic.

How to explain this in an interview

"This switch will break Open-Closed (adding a type should not edit the switch). I will pull a PricingStrategy. Tests stay green so the numbers do not change." Then do it. A diagram of five patterns with the switch still in place does not count.

Wrap-up

You have seen the loop, the extract order, and the Clock trap. Check the ideas, then follow the related notes.

Exercises

  1. ParkingLot.exit reads four Ticket getters to compute a fee. What do you move, and where?
  2. Name the two tests you would keep green while extracting PricingStrategy.
  3. When is a rewrite from scratch the wrong move in a forty-five minute round?

Key takeaway

Treat the first class list as a draft. Keep the green. Extract the unstable algorithm, move methods toward the data, delete leftover. is the loop.