← Back to Design & Development
LLD · In a Hurry · Part 3 of 3

Common LLD Patterns

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.

Why a Short List

Why Only 8 Patterns? (And Not All 23)

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:

Behavioural variation

"This thing has many flavours that we'll keep adding." → Strategy, State, Command

Object construction

"Choosing or building the right object is gnarly." → Factory, Builder, Singleton

Composition

"I want to stack optional features on a base." → Decorator

Notification fan-out

"One thing changes, many things care." → Observer

Pattern as vocabulary, not flex. The senior engineer doesn't drop pattern names to score points; they drop them to compress a design conversation. "I'll use Strategy here" is faster than five sentences explaining the same idea — and it tells the interviewer you've seen this shape before. But the moment the interviewer asks "why?", you'd better be able to translate the name back into the underlying mechanic. Patterns earn points only when they're load-bearing.
The Cheat-Sheet

The Cheat-Sheet — Pattern Smells in One Table

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 30-second drill. Read the smells column out loud, eyes closed, and try to name the pattern. When you can do all eight in 30 seconds without peeking, you can pattern-recognise during a live interview. That's the bar.
Behavioural · #1 most-used

① Strategy — Swap an Algorithm Without Touching Its Caller

StrategyBehavioural

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.

classDiagram class ParkingLot { -PricingStrategy pricing +setPricing(PricingStrategy) +bill(Ticket) Bill } class PricingStrategy { <> +calculate(Duration,VehicleSize) double } class HourlyPricing class WeekendPricing class EvPricing ParkingLot --> PricingStrategy : holds PricingStrategy <|.. HourlyPricing PricingStrategy <|.. WeekendPricing PricingStrategy <|.. EvPricing
Java sketch — Strategy in 15 lines
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 { /* ... */ }
Interview-killer follow-up: "Can I change the strategy at runtime?" Answer: "Yes — the lot exposes 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>."
Creational

② Factory — Hide the "new" From the Caller

FactoryCreational

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.

Java sketch — a switch-expression Factory (Java 17+)
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);
The Factory ↔ Strategy distinction interviewers love to test. Factory chooses which class to instantiate. Strategy chooses which algorithm to call on an already-instantiated object. They're often used together: a Factory builds a Strategy. "PricingFactory.forRegion("US-CA") returns the right PricingStrategy for California."
Creational

③ Singleton — Exactly One, Globally Reachable

SingletonCreational

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.

✅ The right Singleton (Java)

enum ParkingLot {
  INSTANCE;
  private final List<Floor> floors = new ArrayList<>();
  public Ticket park(Vehicle v) { /* ... */ }
}

// Usage
ParkingLot.INSTANCE.park(car);

❌ The broken Singleton

class ParkingLot {
  private static ParkingLot instance;
  public static ParkingLot get() {
    if (instance == null)         // race!
      instance = new ParkingLot();
    return instance;
  }
}
The trap. Singletons are easy to overuse. They're global mutable state, which makes unit testing harder (you can't easily inject a fake) and hides dependencies (the consumer's signature doesn't say "I need a ParkingLot"). Use Singleton only when there's a true one-instance invariant in the domain. For "I want to share an instance", prefer dependency injection — pass it in.
Behavioural

④ Observer — One Publisher, Many Subscribers

ObserverBehavioural

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.

classDiagram class Spot { -List~SpotListener~ listeners +subscribe(SpotListener) +vacate() } class SpotListener { <> +onSpotFreed(Spot) } class SignBoard class ReservationService class AnalyticsService Spot --> SpotListener : notifies SpotListener <|.. SignBoard SpotListener <|.. ReservationService SpotListener <|.. AnalyticsService
Java sketch — Spot fans out to subscribers
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
  }
}
The "synchronous Observer is a footgun" tell. If a subscriber's 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."
Behavioural

⑤ State — Behaviour Changes With the Lifecycle Stage

StateBehavioural

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.

stateDiagram-v2 [*] --> Idle Idle --> CoinInserted : insertCoin() CoinInserted --> Dispensing : selectItem() CoinInserted --> Idle : refund() Dispensing --> Idle : itemDispensed() Idle --> OutOfStock : stockEmpty() OutOfStock --> Idle : restocked()
Java sketch — State pattern in a vending machine
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; }
}
State vs enum-with-switch. Interviewers love asking "why not just an enum and a switch in each method?" Two answers: (1) Adding a new state with the switch approach edits every method — Open/Closed violation. With State, you add one new class. (2) The enum version forces every method to handle every state, even when the state is irrelevant to that event — most cases become "throw IllegalStateException". The State pattern lets each state class only implement what's meaningful.
Structural

⑥ Decorator — Stack Optional Features Around a Base

DecoratorStructural

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.

Java sketch — Decorator stack on PricingStrategy
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)));
Decorator vs Strategy. They look alike on the diagram (both implement an interface, both held by reference) — the difference is composition. Strategy replaces the algorithm. Decorator augments it by wrapping the previous one. If you find yourself writing if (peakHour) usePeakStrategy() else useNormalStrategy(), you're hand-rolling a Decorator badly.
Creational

⑦ Builder — Object Construction When You Have More Than 3 Params

BuilderCreational

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().

Java sketch — Builder for a complex Pizza
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();
The "why not just records?" follow-up. Java records solve the boilerplate problem for value objects with a fixed set of fields. Builder solves a different problem — incremental, optional, validated construction. Use records when every field is required and the order is obvious; use Builder when many fields are optional or when you need cross-field validation in build().
Behavioural

⑧ Command — Wrap an Action so You Can Queue, Log, or Undo It

CommandBehavioural

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.

Java sketch — Command for a text editor's undo
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(); }
}
Command's killer feature is symmetry. Once every action is a Command, you get queueing for free (push to a queue instead of executing immediately), logging for free (serialise the command to disk), distributed execution for free (send the command over the wire). That's why Command is the heart of distributed job systems and event-sourced architectures — a humble whiteboard pattern that scales surprisingly far.
The Real World

Composing Patterns — How They Stack in Real Designs

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.

Parking Lot

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.

Vending Machine

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.

Notification Service

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.

flowchart LR A([Vehicle arrives]) --> F[Factory · build right Spot] F --> S[Singleton · ParkingLot.park] S --> P[Strategy · Pricing] P --> D[Decorator · Surge wraps Pricing] D --> B[Bill] S -.event.-> O[Observer · spot occupied] O --> SB[Sign Board] O --> AS[Analytics] style F fill:#171d27,stroke:#38b265,color:#d4dae5 style S fill:#171d27,stroke:#4a90d9,color:#d4dae5 style P fill:#171d27,stroke:#e8743b,color:#d4dae5 style D fill:#171d27,stroke:#3cbfbf,color:#d4dae5 style O fill:#171d27,stroke:#9b72cf,color:#d4dae5
The narration that scores points: "Each pattern is on its own axis of change. Pricing rules change quarterly — Strategy. Spot subclass varies with vehicle size — Factory. The lot is one-per-facility — Singleton. New observers want to react to spots — Observer. None of them step on each other because they own different axes."
Anti-patterns

Anti-patterns — Pattern Misuse That Loses Loops

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.

🚨 Singleton everywhere

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.

🚨 Strategy with one implementation

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.

🚨 Observer in single-threaded synchronous code

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).

🚨 State pattern for two states

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.

The meta-rule. Patterns are answers, not questions. Never start a design with "let me add a Strategy here" — start with "what's the variation point?" If there's no variation, there's no pattern. The best LLD answers use 2–4 patterns, each one earning its place. The worst use 8 patterns to look smart and end up brittle.