InterviewKata
Low-level design · Module 04 of 11 · Level 1: Foundations

Design patterns you'll actually use

Patterns are named solutions to problems that keep coming back. You don't need all 23 from the book; you need to spot the handful that appear in almost every LLD prompt, and to explain the problem each one solves.

See the patternThis module is the short interview tour. The Design patterns track gives each pattern its own page with a lab and code in three languages: 13 Strategy · 14 State · 18 Observer · 01 Factory method · 07 Decorator · 17 Chain of responsibility · 10 Composite · 03 Builder · 05 Singleton, and why to inject instead.

1. The idea

Chess players don't calculate every move from scratch. They recognise openings and tactics by name ("a fork", "a pin") and reach for the known answer. Design patterns are named answers to problems that keep coming back. Naming them in an interview also saves time: "the fee calculator is a strategy" says a paragraph in four words.

The 1994 Design Patterns book lists 23. These earn their place in almost every LLD interview:

PatternUse it when…Seen in
StrategyOne step has several interchangeable algorithmsP06 fees, P02, P11
StateBehaviour depends on a state that changesP07
ObserverOthers must react to events without the source knowing themDisplays, notifications, P14
FactoryCallers shouldn't choose the concrete classNotifiers, parsers
DecoratorAdd behaviour (logging, caching, auth) around an objectThread-safe wrappers in P01
Chain of responsibilityA request passes along handlers until doneP08 notes
CompositeTreat single items and groups the same wayP15 file tree
BuilderAn object has many optional partsRequests, configurations
SingletonExactly one instance (use sparingly)A process-wide config; prefer injection

2. How it works

The code below in motion: a payment through a decorator, then an event fanned out to observers that a factory created.

3. Try it yourself

Which pattern fits?

Eight problems from real prompts. Pick the pattern you'd reach for, then check.

4. In code

Strategy, observer, factory and decorator in one small program.

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Callable


# Strategy: interchangeable algorithms behind one interface, chosen at runtime.
class ShippingCost(ABC):
    @abstractmethod
    def cost(self, weight_kg: int) -> int: ...


class Standard(ShippingCost):
    def cost(self, weight_kg: int) -> int:
        return 5 + weight_kg


class Express(ShippingCost):
    def cost(self, weight_kg: int) -> int:
        return 15 + 2 * weight_kg


# Observer: the order announces events; listeners subscribe without the order knowing them.
class EventBus:
    def __init__(self) -> None:
        self.listeners: dict[str, list[Callable[[str], None]]] = {}

    def subscribe(self, event: str, fn: Callable[[str], None]) -> None:
        self.listeners.setdefault(event, []).append(fn)

    def publish(self, event: str, payload: str) -> None:
        for fn in self.listeners.get(event, []):
            fn(payload)


# Factory: callers ask for "email" or "sms" and never name a concrete class.
class Notifier(ABC):
    @abstractmethod
    def send(self, msg: str) -> str: ...


class EmailNotifier(Notifier):
    def send(self, msg: str) -> str:
        return f"email: {msg}"


class SmsNotifier(Notifier):
    def send(self, msg: str) -> str:
        return f"sms: {msg}"


def make_notifier(kind: str) -> Notifier:
    makers: dict[str, Callable[[], Notifier]] = {"email": EmailNotifier, "sms": SmsNotifier}
    if kind not in makers:
        raise ValueError(f"unknown notifier {kind}")
    return makers[kind]()


# Decorator: wrap an object in another with the same interface to add behaviour.
class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: int) -> str: ...


class FakeGateway(PaymentGateway):
    def charge(self, amount: int) -> str:
        return f"charged {amount}"


class LoggingGateway(PaymentGateway):
    def __init__(self, inner: PaymentGateway, log: list[str]) -> None:
        self.inner, self.log = inner, log

    def charge(self, amount: int) -> str:
        self.log.append(f"about to charge {amount}")
        result = self.inner.charge(amount)
        self.log.append(f"result: {result}")
        return result


if __name__ == "__main__":
    for strategy in (Standard(), Express()):
        print(f"{type(strategy).__name__} shipping for 3 kg: {strategy.cost(3)}")
    bus = EventBus()
    bus.subscribe("order_paid", lambda o: print(make_notifier("email").send(f"order {o} paid")))
    bus.subscribe("order_paid", lambda o: print(make_notifier("sms").send(f"order {o} paid")))
    log: list[str] = []
    gateway = LoggingGateway(FakeGateway(), log)
    print(gateway.charge(42))
    bus.publish("order_paid", "#7")
    print("log:", " | ".join(log))
// Strategy: interchangeable algorithms behind one interface, chosen at runtime.
interface ShippingCost { readonly name: string; cost(weightKg: number): number; }
class Standard implements ShippingCost { readonly name = "Standard"; cost(w: number): number { return 5 + w; } }
class Express implements ShippingCost { readonly name = "Express"; cost(w: number): number { return 15 + 2 * w; } }

// Observer: the order announces events; listeners subscribe without the order knowing them.
class EventBus {
  private listeners = new Map<string, ((payload: string) => void)[]>();
  subscribe(event: string, fn: (payload: string) => void): void {
    this.listeners.set(event, [...(this.listeners.get(event) ?? []), fn]);
  }
  publish(event: string, payload: string): void {
    for (const fn of this.listeners.get(event) ?? []) fn(payload);
  }
}

// Factory: callers ask for "email" or "sms" and never name a concrete class.
interface Notifier { send(msg: string): string; }
class EmailNotifier implements Notifier { send(msg: string): string { return `email: ${msg}`; } }
class SmsNotifier implements Notifier { send(msg: string): string { return `sms: ${msg}`; } }
function makeNotifier(kind: string): Notifier {
  const makers: Record<string, () => Notifier> = { email: () => new EmailNotifier(), sms: () => new SmsNotifier() };
  const make = makers[kind];
  if (!make) throw new Error(`unknown notifier ${kind}`);
  return make();
}

// Decorator: wrap an object in another with the same interface to add behaviour.
interface PaymentGateway { charge(amount: number): string; }
class FakeGateway implements PaymentGateway { charge(amount: number): string { return `charged ${amount}`; } }
class LoggingGateway implements PaymentGateway {
  constructor(private inner: PaymentGateway, private log: string[]) {}
  charge(amount: number): string {
    this.log.push(`about to charge ${amount}`);
    const result = this.inner.charge(amount);
    this.log.push(`result: ${result}`);
    return result;
  }
}

for (const s of [new Standard(), new Express()]) console.log(`${s.name} shipping for 3 kg: ${s.cost(3)}`);
const bus = new EventBus();
bus.subscribe("order_paid", (o) => console.log(makeNotifier("email").send(`order ${o} paid`)));
bus.subscribe("order_paid", (o) => console.log(makeNotifier("sms").send(`order ${o} paid`)));
const log: string[] = [];
const gateway = new LoggingGateway(new FakeGateway(), log);
console.log(gateway.charge(42));
bus.publish("order_paid", "#7");
console.log("log:", log.join(" | "));
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;

// Strategy: interchangeable algorithms behind one interface, chosen at runtime.
interface ShippingCost {
    int cost(int weightKg);
}

class Standard implements ShippingCost {
    public int cost(int w) { return 5 + w; }
}

class Express implements ShippingCost {
    public int cost(int w) { return 15 + 2 * w; }
}

// Observer: the order announces events; listeners subscribe without the order knowing them.
class EventBus {
    private final Map<String, List<Consumer<String>>> listeners = new HashMap<>();

    void subscribe(String event, Consumer<String> fn) {
        listeners.computeIfAbsent(event, e -> new ArrayList<>()).add(fn);
    }

    void publish(String event, String payload) {
        listeners.getOrDefault(event, List.of()).forEach(fn -> fn.accept(payload));
    }
}

// Factory: callers ask for "email" or "sms" and never name a concrete class.
interface Notifier {
    String send(String msg);
}

class EmailNotifier implements Notifier {
    public String send(String msg) { return "email: " + msg; }
}

class SmsNotifier implements Notifier {
    public String send(String msg) { return "sms: " + msg; }
}

class Notifiers {
    private static final Map<String, Supplier<Notifier>> MAKERS = Map.of("email", EmailNotifier::new, "sms", SmsNotifier::new);

    static Notifier make(String kind) {
        Supplier<Notifier> m = MAKERS.get(kind);
        if (m == null) throw new IllegalArgumentException("unknown notifier " + kind);
        return m.get();
    }
}

// Decorator: wrap an object in another with the same interface to add behaviour.
interface PaymentGateway {
    String charge(int amount);
}

class FakeGateway implements PaymentGateway {
    public String charge(int amount) { return "charged " + amount; }
}

class LoggingGateway implements PaymentGateway {
    private final PaymentGateway inner;
    private final List<String> log;

    LoggingGateway(PaymentGateway inner, List<String> log) {
        this.inner = inner;
        this.log = log;
    }

    public String charge(int amount) {
        log.add("about to charge " + amount);
        String result = inner.charge(amount);
        log.add("result: " + result);
        return result;
    }
}

public class Main {
    public static void main(String[] args) {
        for (ShippingCost s : List.of(new Standard(), new Express()))
            System.out.println(s.getClass().getSimpleName() + " shipping for 3 kg: " + s.cost(3));
        EventBus bus = new EventBus();
        bus.subscribe("order_paid", o -> System.out.println(Notifiers.make("email").send("order " + o + " paid")));
        bus.subscribe("order_paid", o -> System.out.println(Notifiers.make("sms").send("order " + o + " paid")));
        List<String> log = new ArrayList<>();
        PaymentGateway gateway = new LoggingGateway(new FakeGateway(), log);
        System.out.println(gateway.charge(42));
        bus.publish("order_paid", "#7");
        System.out.println("log: " + String.join(" | ", log));
    }
}

Output (the same in all three languages)

Standard shipping for 3 kg: 8
Express shipping for 3 kg: 21
charged 42
email: order #7 paid
sms: order #7 paid
log: about to charge 42 | result: charged 42

5. In the interview

6. Remember it

Memory hook: "Swap, change, notify, create, wrap"Strategy swaps an algorithm, state changes behaviour with state, observer notifies, factory creates, decorator wraps.
How this hook works: what “Swap, change, notify, create, wrap” means

What the slogan means, phrase by phrase

“Swap”
Strategy: choose an algorithm at runtime.
“change”
State: the object's behaviour changes with its state.
“notify”
Observer: tell subscribers about events.
“create”
Factory: hide which class gets built.
“wrap”
Decorator: add behaviour around an object.

Why this picture fits: Five verbs for the five patterns you'll use most. The verb is the problem; the pattern is the answer.

The story → the concept

The verbThe pattern
Swap an algorithmStrategy
Change behaviour as state changesState
Notify others of an eventObserver
Create without naming the classFactory
Wrap to add behaviourDecorator

Watch it on an example

  1. "Shipping cost depends on the option chosen" → swap → Strategy.
  2. "Log every payment without changing the gateway" → wrap → Decorator.
  3. "Email and SMS when an order is paid" → notify, and create the notifier by name → Observer + Factory.

The trap it saves you from

Memorising 23 diagrams and using none of them well.

It still works when…

The problem doesn't match a verb. Then you probably don't need a pattern.

7. Common mistakes

Pattern first, problem secondA strategy with one implementation that will never change is ceremony.
Singleton for convenienceIt's global state; tests interfere with each other. Inject one instance instead.
Confusing strategy and stateIn state, the states decide the next state; in strategy, the client picks the algorithm.
Observers that throwOne failing subscriber shouldn't stop the others; catch and log per listener.

8. Practice exercise (25 minutes)

  1. Add a strategy (5 min). A Freight shipping cost in the code below.
  2. Add a decorator (10 min). A RetryingGateway that retries a failed charge once.
  3. Name the patterns (10 min). In two problems from this track, list every pattern used and the problem it solves.

9. Self-check quiz

You need to add logging around a payment gateway without editing it. Which pattern?

Wrap it in an object with the same interface.

A vending machine behaves differently when idle or holding money. Which pattern?

Behaviour depends on a state that changes.

Email and SMS services must react when an order is paid, without the order knowing them. Which pattern?

Subscribers register for the event.

Why is Singleton often discouraged in interviews?

Inject a single instance instead.

10. Go deeper

ResourceTimeWhy
The pattern catalogue (refactoring.guru)ReferenceEvery classic pattern with diagrams and code.
Module 05: Class and sequence diagramsNextHow to draw these patterns on a whiteboard.
P07 and P0890 minState and chain of responsibility in full problems.