SwadhyAI Notes

All Low-Level Design notes

Production LLD

From smells to patterns

Friday standup. Product wants EV pricing by Monday. You open fee() and find a huge switch on vehicle type. Adding EV is another case. The same constructor already picks among six concretes, a status enum's methods are all branches, and one subject hard-codes three listeners.

Those are design smells: concrete warnings that a known pattern may help. The pattern is the after picture of a refactor, not a decoration you sprinkle on a clean class.

Direction of travel

1

See the smell

Spot it on the board: switch, blob, double, or chain.

2

Name it out loud

, , , or shotgun surgery (one change hitting many files).

3

Extract the smallest fix

Keep tests green. Clock stays a port (an interface) so fee tests do not rot.

4

Then stop

Stop when the follow-up can add a class. Do not decorate further.

What actually happens when you start from the pattern name

The Friday switch is not a puzzle you solve by reciting a catalog. A first-time reader who jumps to "I will use " on a problem with no listeners is doing cargo cult (copying a famous name because it sounds like design). Starting from "this switch will grow" and extracting the varying algorithm is engineering.

Each smell already points at a smallest honest fix. A type switch that chooses an algorithm wants . The same new Concrete() in three places wants (or a registry: a lookup of constructors). A status enum whose methods are all branches wants only when each status has real behavior. A hardcoded listener list wants the subscriber interface from the paragraph above.

Not every warning is a Gang of Four (GoF) sticker. becomes a ; a becomes several types and a thin controller; becomes a method move. Those are still refactors. The mapping table below is the catalog; the extracts after it show the first four rows in Java.

Simple rule of thumb

After you can see the Friday switch, the habit is one sentence: smell first, pattern second.

Pick the smallest pattern or move that removes the warning. Name it out loud so the follow-up is obvious. Extract with tests still green. Stop when the next vehicle type, listener, or status is a new class, not another edit to the blob.

In simpler terms

Starting with "I will use " on a problem with no listeners is cargo cult. Starting with "this switch will grow" and extracting is engineering.

Mapping

Which smell, which smallest honest fix?

The rule above needs a lookup. Each row is a smell you can point at on the board, the smallest fix that removes it, and when that pairing shows up in parking-lot and vending problems.

Switch on type choosing an algorithm

Fees, allocation, split rules, or win rules that will keep growing.

Same new Concrete() switch in three places

/ registry

Spots, vehicles, or handlers constructed the same way in several methods.

Status enum with a page of branches

Vending or ticket lifecycle where each status has real behavior.

Hardcoded listener list

Notify, metrics, and boards that should subscribe without the subject naming them.

Nested and/or business rules

Specification + Composite (combinable rule objects)

Eligibility and discount rules you combine with and/or instead of nesting.

Copy-pasted wrappers

Logging, retry, or compression stacked around one port interface.

Primitive obsession (double, String)

Money, Email, or TicketId instead of a raw double or String.

God object

Split types + Controller

A manager that owns every verb; split into types and a thin controller.

Feature envy

Move method

A lot computing a ticket fee from getters; move toward the expert.

The first four rows, plus wrappers, in Java

The table names the pairing. These extracts are the after picture: same behavior, new home for the variation. Titles stay with the samples so you can match each smell to its refactor.

Switch becomes Strategy

java
// smell: switch on type
Money fee(Vehicle v, long hours) {
  switch (v.type()) {
    case CAR: return hourly.times(hours);
    case EV:  return evHourly.times(hours);
    default:  throw new IllegalArgumentException();
  }
}

// refactor: Strategy
Money fee(Vehicle v, long hours) {
  return pricingFor(v).fee(hours);
}

interface PricingStrategy {
  Money fee(Duration stay);
}

class EvPricing implements PricingStrategy {
  public Money fee(Duration stay) {
    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 EV_HOURLY.times(hours);
  }
}

Construction becomes Factory

java
// smell: construction duplicated
Spot spot = type.equals("COMPACT") ? new CompactSpot(id) : new LargeSpot(id);

// refactor: Factory
Spot spot = spotFactory.create(type, id);

Status branches become State

java
// smell: status field with a page of branches
if (status == OPEN) { /* park rules */ }
else if (status == PAID) { /* exit rules */ }
else if (status == CLOSED) { /* reject */ }

// refactor when each status has behavior
ticket.state().exit(clock.instant());

Hardcoded listeners become Observer

java
// smell
emailer.notify(ticket);
board.update(ticket);
metrics.inc();

// refactor
for (TicketListener listener : listeners) {
  listener.onIssued(ticket);
}

Decorator when wrappers copy-paste

is the copy-pasted-wrapper row, not a with flags. LoggingPaymentGateway and RetryingPaymentGateway wrap the same port (the PaymentGateway interface). Each wrapper implements that port and delegates. Tests inject a fake inner gateway.

java
class LoggingPaymentGateway implements PaymentGateway {
  private final PaymentGateway inner;
  public PaymentResult charge(OrderId id, Money amount) {
    PaymentResult result = inner.charge(id, amount);
    log.info("charged {} {}", id, amount);
    return result;
  }
}

Not every smell is GoF

Value objects, method moves, thin controllers

The last three rows in the mapping are not Gang of Four (GoF) catalog entries. They are still the after picture of a refactor; they just do not need a famous name sprinkled on the class.

Becomes a . Money, Email, TicketId. Not .

Becomes several types and a thin controller. Not a Facade (a thin front that delegates) sticker on the same blob.

Becomes a method move toward the information expert (the type that already has the data). Sometimes that expert is a policy object.

How to explain this, and where the extract still fails

Once you can point at a row in the mapping and show the Java after picture, the interview is whether you can narrate one extract without decorating past the smell.

How to explain this in an interview

"This switch will break Open-Closed (open for extension, closed for modification). That is shotgun surgery waiting to happen. I will pull a PricingStrategy. shrinks because pricing is no longer a reason to edit ParkingLot. on double hours dies because the strategy returns Money. dies because the lot stops reading Ticket getters to do arithmetic."

One extract, four smells named, tests still on park and lot-full. That is the page in one paragraph.

Common mistakes

Pattern name without an extracted type

Saying while the switch remains. The pattern is the interface plus implementations, not the word.

State pattern for a two-value flag

A boolean paid is not Idle/HasCoin/Sold. Enum plus guards can be enough. Use when each status has real behavior.

Observer with no subscribers in the problem

If nobody listens, a direct call is honest. Extract when the second listener appears.

Wrap-up

You have the lookup, the extracts, and the spoken paragraph. Check the ideas stuck, then follow the related notes.

Check yourself

  1. A vending machine has Idle, HasCoin, Sold. Smell? Pattern?
  2. double amount in three methods. Is the fix or a ?
  3. Narrate one extract: smell → name → interface → keep tests green.

Key takeaway

Let the smell choose the pattern. Use the mapping above as the catalog, and stop when the follow-up can add a class.