Design patterns

Design patterns for LLD

Creational, structural, and behavioral patterns worth knowing for interviews: Factory, Builder, Singleton caveats, Decorator, Facade, Strategy, Observer, State.

Patterns are names, not a checklist

OOP core → guidelines + STAR → this page → classic E2E designs.
The Gang of Four catalog defined 23 patterns in 1994. Most don’t matter in modern LLD interviews — languages absorbed some (iterators), and composition/FP made others rare. Interviewers usually care about a handful. Use a pattern when the problem calls for it; forcing one is over-engineering.

Creational — Factory, Builder, Singleton

Factory (simple factory). Hide which concrete type gets created. Common when requirements say “support different notification types” or payment methods. Check the room — some interviewers grimace at factories in languages where they’re less idiomatic.
def create_notification(kind: str) -> Notification:
    if kind == "email":
        return EmailNotification()
    if kind == "sms":
        return SMSNotification()
    raise ValueError(kind)


notif = create_notification("email")
notif.send("Hello")
Notification createNotification(String kind) {
    if ("email".equals(kind)) return new EmailNotification();
    if ("sms".equals(kind)) return new SMSNotification();
    throw new IllegalArgumentException(kind);
}

Notification notif = createNotification("email");
notif.send("Hello");
Builder. Stepwise construction for objects with many optional parts (HTTP requests, query builders). Rare for simple domain objects with 2–4 required fields — a normal constructor is enough.
Singleton. Exactly one instance. Usually pass shared dependencies through constructors instead — clearer and easier to test. Know the pattern; default answer to “should this be a Singleton?” is often no.

Structural — Decorator, Facade

Decorator. Layer behavior at runtime without subclass explosion (logging + encryption + compression wrappers). Think “optional stackable features.” If the variation is a stable type difference, a subclass or Strategy may be clearer.
source: DataSource = FileDataSource("data.txt")
source = EncryptionDecorator(source)
source = CompressionDecorator(source)
source.write("sensitive info")
# write path: compress → encrypt → file
DataSource source = new FileDataSource("data.txt");
source = new EncryptionDecorator(source);
source = new CompressionDecorator(source);
source.write("sensitive info");
// write path: compress → encrypt → file
Facade. A coordinator that hides internals. Your Game class in Tic-Tac-Toe is a facade. Almost nobody names it in interviews — and that’s fine. Build clean orchestrators; name Facade only when it helps (e.g. wrapping a messy legacy subsystem).

Behavioral — Strategy, Observer, State

Strategy. The pattern interviewers love most. Swap algorithms/behaviors via composition instead of if/else on type. If you learn one pattern deeply, make it this.
class PaymentStrategy(Protocol):
    def pay(self, amount: float) -> bool: ...


class ShoppingCart:
    def __init__(self):
        self._strategy: PaymentStrategy | None = None

    def set_payment(self, strategy: PaymentStrategy) -> None:
        self._strategy = strategy

    def checkout(self, amount: float) -> None:
        assert self._strategy is not None
        self._strategy.pay(amount)
interface PaymentStrategy {
    boolean pay(double amount);
}

class ShoppingCart {
    private PaymentStrategy strategy;

    void setPayment(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    void checkout(double amount) {
        assert strategy != null;
        strategy.pay(amount);
    }
}
Observer. Multiple components react to one change — stock tick updates displays and alerts; order placement notifies inventory and analytics. Keywords: notify, update many listeners.
class Stock:
    def __init__(self, symbol: str):
        self.symbol = symbol
        self._price = 0.0
        self._observers: list[Observer] = []

    def attach(self, observer: Observer) -> None:
        self._observers.append(observer)

    def set_price(self, price: float) -> None:
        self._price = price
        for o in self._observers:
            o.update(self.symbol, price)
class Stock {
    final String symbol;
    private double price = 0.0;
    private final List<Observer> observers = new ArrayList<>();

    Stock(String symbol) { this.symbol = symbol; }

    void attach(Observer observer) {
        observers.add(observer);
    }

    void setPrice(double price) {
        this.price = price;
        for (Observer o : observers) {
            o.update(symbol, price);
        }
    }
}
State machine (State pattern). Behavior depends on internal state with non-trivial transitions — vending machines, document workflows, game phases. If “state” dominates the requirements, this is often the centerpiece. A quick state diagram (circles + labeled arrows) communicates instantly.
# Each state object knows valid actions + next state
machine.select_product()  # "insert coin first"
machine.insert_coin()     # -> HasCoin
machine.select_product()  # -> Dispense
machine.dispense()        # -> NoCoin
// Each state object knows valid actions + next state
machine.selectProduct();  // "insert coin first"
machine.insertCoin();     // -> HasCoin
machine.selectProduct();  // -> Dispense
machine.dispense();       // -> NoCoin

Cheat sheet

  • Factory — callers shouldn’t pick the concrete class
  • Builder — messy / optional construction
  • Singleton — truly one global instance (rare)
  • Decorator — stack optional runtime behaviors
  • Facade — simple entry over complexity
  • Strategy — interchangeable behaviors / kill if-else type switches
  • Observer — many react to one event
  • State — transitions get messy; encapsulate per state

Worked mini-example: Rate limiter Strategy + Factory

Limiter is Strategy; creation from config is Factory; RateLimiter orchestrator doesn’t switch on algorithm mid-allow.

class Limiter(Protocol):
    def allow(self) -> bool: ...

def create_limiter(kind: str, cfg) -> Limiter:
    if kind == "token_bucket":
        return TokenBucket(cfg)
    if kind == "sliding_log":
        return SlidingLog(cfg)
    raise ValueError(kind)

class RateLimiter:
    def allow(self, key: str) -> bool:
        return self._for(key).allow()
interface Limiter {
    boolean allow();
}

Limiter createLimiter(String kind, Config cfg) {
    if ("token_bucket".equals(kind)) return new TokenBucket(cfg);
    if ("sliding_log".equals(kind)) return new SlidingLog(cfg);
    throw new IllegalArgumentException(kind);
}

class RateLimiter {
    boolean allow(String key) {
        return forKey(key).allow();
    }
}

Pattern anti-patterns

  • Strategy with one algorithm and no foreseeable second.
  • Observer before the subject’s invariants work.
  • Decorator stacks that obscure the core verb.
  • Singleton everything — hidden global mutable state.

Pattern pick checklist

  1. Creational — is wiring messy or multi-product?
  2. Structural — am I adapting an interface or composing behavior?
  3. Behavioral — does the algorithm, notification, or workflow vary?
  4. If none apply, draw plain classes. Patterns are optional tools.

Common interview pitfalls

These mistakes show up constantly on this prompt. Name the trap, then show the fix in your design — don’t wait for the interviewer to catch you.

  • Catalog dump in minute one.
  • Strategy with a single implementation.
  • Factory that is just a constructor alias.
  • Observer memory leaks / missing unsubscribe narrative.
  • Decorator used to hide bad API design.

Interview script (say this)

Read this once out loud before a mock. It’s the spine of a strong answer — not a script to recite robotically.

  1. I only reach for patterns when something varies.
  2. Creational: Factory when config picks implementations.
  3. Structural: Adapter when integrating an awkward API.
  4. Behavioral: Strategy for swappable algorithms; Observer for fan-out events.
  5. Rate limiter is my go-to Strategy+Factory example.
  6. Logger destinations are another clean Strategy/composition example.
  7. If v1 has one path, I’ll note the seam without implementing three classes.
  8. Ask me which pattern you want to stress — I’ll wire it into the design.

Extra verification traces

Walk these three traces on the board. If you can narrate them cleanly, your implementation section usually follows.

Staff-level follow-ups

At staff+, they twist the prompt. Answer in one sentence that names the seam — don’t redesign the whole board.

  • Too much abstraction? — Inline until second implementation appears.
  • DI frameworks? — Manual wiring on whiteboard; mention DI as production concern.
  • Pattern vs principle? — Principles guide shape; patterns name recurring shapes.

← Lattice