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
See the smell
Spot it on the board: switch, blob, double, or chain.
Name it out loud
, , , or shotgun surgery (one change hitting many files).
Extract the smallest fix
Keep tests green. Clock stays a port (an interface) so fee tests do not rot.
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
// 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
// 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
// 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
// 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.
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
- A vending machine has Idle, HasCoin, Sold. Smell? Pattern?
- double amount in three methods. Is the fix or a ?
- 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.