The eight GoF patterns that show up in 90% of LLD interviews — when each one fits, the smell that summons it, and a one-page Java example.
The original GoF book lists 23 patterns. If you tried to memorise all of them before an interview you'd burn out and remember none. The good news: most LLD interviews surface the same eight. The other fifteen exist — Bridge, Flyweight, Memento, Visitor, Mediator and friends — but if you reach for one of those in a 45-minute interview, the interviewer will ask "why?" and you'd better have a sharp answer because they're rarely the cleanest fit.
The eight on this page cover the four problem shapes that almost every LLD prompt reduces to:
"This thing has many flavours that we'll keep adding." → Strategy, State, Command
"Choosing or building the right object is gnarly." → Factory, Builder, Singleton
"I want to stack optional features on a base." → Decorator
"One thing changes, many things care." → Observer
This is the table to keep open during practice. When you see the smell on the left, the pattern on the right is almost always the right reach. Skim it now; the rest of the page details each row.
| The Smell (in plain English) | Pattern | Canonical LLD Scenario |
|---|---|---|
An if/else ladder over a "kind" enum |
Strategy | Pricing rules in a parking lot, sort algorithms in a list |
| "Which subclass do I new-up?" logic in a constructor | Factory | Creating the right Spot subclass from VehicleSize |
| "I need exactly one of these, accessible globally" | Singleton | The ParkingLot registry, a config holder, a connection pool |
| "One write must update many readers" (no return value) | Observer | Spot freed → notify reservation holders; price change → push to UI |
| Behaviour changes based on a lifecycle stage | State | Vending machine: IDLE → COIN_INSERTED → DISPENSING |
| "Wrap a base with optional features that stack" | Decorator | Pricing → SurgeDecorator(pricing) → DiscountDecorator(...) |
| Constructor with >3 params and many of them optional | Builder | Configuring a Pizza, an HTTP request, a complex Game |
| "I need to undo / queue / log this action" | Command | Text-editor undo, scheduled job dispatch, macro recording |
The story. Sarah's parking lot launches with one flat hourly rate. Three months later marketing wants weekend pricing. Six months later legal demands a separate EV rate. Without a pattern, every new rule means editing ParkingLot.calculateBill() — by year two it's a 400-line method nobody dares touch. Strategy says: pull the algorithm out into its own object behind an interface, hold the interface by reference, and let new rules be new files.
When to reach for it. Whenever you have an if/else ladder over a "kind" enum where each branch does the same kind of work (compute, validate, route, format) but with different rules.
The mechanic. One interface with one method (calculate, route, format). Many implementations. The host class holds the interface as a field — set in the constructor, swappable at runtime.
interface PricingStrategy { double calculate(Duration stay, VehicleSize size); } class HourlyPricing implements PricingStrategy { private final double rate; HourlyPricing(double rate) { this.rate = rate; } public double calculate(Duration d, VehicleSize s) { return Math.ceil(d.toMinutes() / 60.0) * rate; } } // Adding weekend pricing = one new file. Zero edits to ParkingLot. class WeekendPricing implements PricingStrategy { /* ... */ }
setPricing(), which is just a field assignment. As long as no thread is mid-calculation, it's a single CAS. For perfectly atomic switchover, wrap the field in AtomicReference<PricingStrategy>."The story. The parking lot's allocator needs a Spot for a vehicle of size CAR. Without a Factory, every caller writes if (size == CAR) new CarSpot() else if (size == TRUCK) new TruckSpot() .... That switch lives in five places. Add an EV spot — five edits. Forget one — production bug. A Factory centralises the switch in one method so new types add a single branch in a single file.
When to reach for it. When (a) the caller doesn't (and shouldn't) know the concrete subclass, (b) the choice depends on input data, and (c) you want to add new variants without touching the call sites.
The mechanic. A static method (or a dedicated factory class) takes input parameters and returns the abstract type, internally choosing the concrete subclass.
class SpotFactory { static Spot create(String id, VehicleSize size) { return switch (size) { case BIKE -> new BikeSpot(id); case CAR -> new CarSpot(id); case TRUCK -> new TruckSpot(id); }; } } // Caller never sees the subclasses Spot s = SpotFactory.create("S-42", VehicleSize.CAR);
PricingFactory.forRegion("US-CA") returns the right PricingStrategy for California."The story. The parking lot facility has exactly one ParkingLot object — there isn't a second copy somewhere. If two threads each instantiated their own ParkingLot, you'd have two separate ticket registries and two cars could legally occupy the same physical spot. Singleton enforces "there is one" at the language level so the rest of the code can't accidentally violate the invariant.
When to reach for it. Configuration holders, registry-style services (ParkingLot, Logger), connection pools, or any global resource that must be shared.
The mechanic. Private constructor + a static accessor that returns the one instance. In Java the cleanest version is the enum singleton — thread-safe, serialisation-safe, free.
enum ParkingLot { INSTANCE; private final List<Floor> floors = new ArrayList<>(); public Ticket park(Vehicle v) { /* ... */ } } // Usage ParkingLot.INSTANCE.park(car);
class ParkingLot { private static ParkingLot instance; public static ParkingLot get() { if (instance == null) // race! instance = new ParkingLot(); return instance; } }
The story. A spot becomes free at 11:42pm. Three things should happen, all asynchronous to the vacating driver: (1) the digital signboard near the entrance updates the "spaces available" count, (2) any reservations for that spot get confirmed, (3) the analytics service logs the duration. Without Observer, Spot.vacate() would have to know about all three and call each one — adding a fourth subscriber means editing vacate(). Observer flips it: vacate() just emits an event, and any object that wants to know subscribes.
When to reach for it. Pub/sub, notifications, UI redraws on data change, anything where one write fans out to many readers and you don't want the writer to know who's listening.
The mechanic. A Subject holds a list of Observer references. subject.notify(event) iterates and calls observer.onEvent(...) on each. Subscribers register/unregister themselves; the subject doesn't know what they do with the event.
interface SpotListener { void onSpotFreed(Spot s); } abstract class Spot { private final List<SpotListener> listeners = new CopyOnWriteArrayList<>(); public void subscribe(SpotListener l) { listeners.add(l); } public void vacate() { occupant.set(null); listeners.forEach(l -> l.onSpotFreed(this)); // fire-and-forget } }
onSpotFreed() is slow (say, an HTTP call), vacate() blocks until it returns. In production you'd push the event onto a queue and have subscribers consume from there. Mention this even if not asked — "in production I'd publish to a Kafka topic; for the LLD I'm keeping it inline so the call stack is visible."The story. A vending machine's insertCoin() button does different things depending on what's happening: in IDLE it accepts the coin and moves to COIN_INSERTED; in DISPENSING it rejects the coin; in OUT_OF_STOCK it returns the coin. Without State, every method is an if/else over the current stage and adding a new stage means editing every method. State pulls the per-stage logic into its own class so adding a stage means adding a class — the existing classes are unchanged.
When to reach for it. Any entity with 3+ lifecycle stages where the same action means something different in each stage. Vending machine, ATM, order/booking lifecycles, traffic light, document workflow.
The mechanic. An interface (VendingState) with one method per event (insertCoin, selectItem, dispense). Each state is a class implementing the interface. The context (VendingMachine) holds the current state and delegates each event to it. Transitions happen by the state object reassigning the context's state field.
interface VendingState { void insertCoin(VendingMachine m, Coin c); void selectItem(VendingMachine m, String code); } class IdleState implements VendingState { public void insertCoin(VendingMachine m, Coin c) { m.addBalance(c); m.setState(new CoinInsertedState()); // state owns the transition } public void selectItem(VendingMachine m, String code) { throw new IllegalStateException("Insert coin first"); } } class VendingMachine { private VendingState state = new IdleState(); public void insertCoin(Coin c) { state.insertCoin(this, c); } public void selectItem(String code) { state.selectItem(this, code); } public void setState(VendingState s) { this.state = s; } }
The story. The parking lot has hourly pricing. Then peak-hour surcharge. Then weekend discount. Then a corporate flat rate that overrides everything. Naively you'd subclass HourlyPricing for each combination — but with three orthogonal modifiers you'd need eight subclasses (2³). Decorator lets each modifier be its own class that wraps a strategy and tweaks the result, so the combinations multiply without the class count exploding.
When to reach for it. Optional features that combine in any order, and you want to compose them at runtime rather than choose a subclass at compile time.
The mechanic. The decorator implements the same interface as the thing it wraps and holds a reference to it. Its method calls the wrapped object first, then modifies the result (or short-circuits it). Decorators stack — each one wraps the previous.
class PeakHourSurcharge implements PricingStrategy { private final PricingStrategy wrapped; PeakHourSurcharge(PricingStrategy w) { this.wrapped = w; } public double calculate(Duration d, VehicleSize s) { double base = wrapped.calculate(d, s); return isPeakHour() ? base * 1.30 : base; } } class WeekendDiscount implements PricingStrategy { private final PricingStrategy wrapped; WeekendDiscount(PricingStrategy w) { this.wrapped = w; } public double calculate(Duration d, VehicleSize s) { double base = wrapped.calculate(d, s); return isWeekend() ? base * 0.80 : base; } } // Compose at runtime — order matters PricingStrategy p = new WeekendDiscount( new PeakHourSurcharge( new HourlyPricing(40.0)));
if (peakHour) usePeakStrategy() else useNormalStrategy(), you're hand-rolling a Decorator badly.The story. A pizza has a size, crust, sauce, cheese, up to 8 toppings, an optional stuffed crust, an optional dessert add-on. Modelling this with a constructor means an 11-parameter Pizza(...) call — the caller can't tell which boolean is "extra cheese" vs "extra mushrooms" without counting positions. Builder gives you a fluent step-by-step build that reads like the order: new PizzaBuilder().size(LARGE).crust(THIN).addTopping(MUSHROOM).build().
When to reach for it. Constructors with more than 3 parameters, especially when many are optional or have sensible defaults. Configuring a parking lot at startup, building an HTTP request, configuring a chess game.
The mechanic. A nested static Builder class with one setter per field that returns this, terminating in build() which returns the constructed (immutable) object. Validation happens in build().
class Pizza { private final Size size; private final Crust crust; private final List<Topping> toppings; private Pizza(Builder b) { // private — only Builder constructs this.size = b.size; this.crust = b.crust; this.toppings = List.copyOf(b.toppings); } public static class Builder { private Size size = Size.MEDIUM; private Crust crust = Crust.REGULAR; private final List<Topping> toppings = new ArrayList<>(); public Builder size(Size s) { this.size = s; return this; } public Builder crust(Crust c) { this.crust = c; return this; } public Builder addTopping(Topping t) { toppings.add(t); return this; } public Pizza build() { if (toppings.size() > 8) throw new IllegalStateException("Max 8"); return new Pizza(this); } } } Pizza p = new Pizza.Builder() .size(Size.LARGE).crust(Crust.THIN) .addTopping(Topping.MUSHROOM).addTopping(Topping.PEPPER) .build();
build().The story. A text editor needs Ctrl+Z. Naively you'd add an undo() method to every action, but then you have to remember the inverse of every action, scattered everywhere. Command says: every user action becomes a Command object that knows how to execute() itself and how to undo() itself. Push every executed command onto a stack; Ctrl+Z pops and calls undo(). Now undo, redo, replay, macro recording, and remote execution are the same problem.
When to reach for it. Whenever an action needs to be queued (job dispatcher), logged for replay (event sourcing), undone (text editor, paint app), or sent across a network (RPC).
The mechanic. An interface (Command) with execute() and optionally undo(). Each command class encapsulates the receiver (what to act on) and the args (the state needed to undo). The invoker holds a list of commands and executes them on demand.
interface Command { void execute(); void undo(); } class InsertTextCommand implements Command { private final Document doc; private final int pos; private final String text; InsertTextCommand(Document d, int p, String t) { doc=d; pos=p; text=t; } public void execute() { doc.insert(pos, text); } public void undo() { doc.delete(pos, text.length()); } } class Editor { private final Deque<Command> history = new ArrayDeque<>(); public void run(Command c) { c.execute(); history.push(c); } public void undoLast() { history.pop().undo(); } }
The interview move that separates senior from mid-level isn't naming patterns in isolation — it's showing how two or three of them compose to solve one problem. Real designs always layer patterns. Here are three common stacks you'll see across the LLD case studies on this site.
Singleton for the lot itself · Factory creates the right Spot subclass · Strategy for pricing rules · Decorator for surge/discount on top of pricing · Observer when a spot frees up.
The lesson: each pattern handles one variation axis. Patterns don't fight each other; they sit on different axes of change.
State pattern owns the lifecycle (Idle → CoinInserted → Dispensing) · Strategy for refund-vs-dispense logic per item · Singleton for the machine instance · Observer for stock-low alerts to the operator's dashboard.
The lesson: State handles how the machine behaves, Strategy handles which policy applies. They're complementary, not competing.
Strategy per channel (Email, SMS, Push) · Factory picks the channel from the request · Decorator wraps a channel with retry+backoff · Observer for delivery-status callbacks · Command when notifications go through a queue.
The lesson: marketplace-style designs always stack 4+ patterns because they have many independent variation axes.
Knowing patterns is half the skill; knowing when not to use them is the other half. Here are the four patterns-mistakes interviewers actively flag.
Candidate makes PricingStrategy, NotificationService, and UserRepository all Singletons. Every dependency is now hidden global state, every unit test needs an awkward reset hook, and any swap-for-a-fake requires reflection. Singleton is for true one-instance invariants (the parking lot, a hardware controller); for "I want to share an instance", use dependency injection — pass it in.
Candidate creates PricingStrategy interface but only ever implements HourlyPricing, with no concrete plan to add another. That's premature abstraction — extra indentation for no flexibility you'll actually use. Wait until you have at least one real second variant or a clearly imminent one before introducing the interface.
Candidate adds Observer to a system where the publisher and subscribers always run on the same thread. The "decoupling" is illusory — the publisher still pays the full latency of every subscriber. Observer earns its keep when subscribers are unknown to the publisher and can run independently (different threads, different services, async).
Candidate uses State pattern for a Spot that only has FREE and OCCUPIED. That's two if-statements wearing five files of overhead. State earns its weight at 3+ states with non-trivial per-state behaviour. For binary, just use a boolean field.