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.
1. The idea
The 1994 Design Patterns book lists 23. These earn their place in almost every LLD interview:
| Pattern | Use it when… | Seen in |
|---|---|---|
| Strategy | One step has several interchangeable algorithms | P06 fees, P02, P11 |
| State | Behaviour depends on a state that changes | P07 |
| Observer | Others must react to events without the source knowing them | Displays, notifications, P14 |
| Factory | Callers shouldn't choose the concrete class | Notifiers, parsers |
| Decorator | Add behaviour (logging, caching, auth) around an object | Thread-safe wrappers in P01 |
| Chain of responsibility | A request passes along handlers until done | P08 notes |
| Composite | Treat single items and groups the same way | P15 file tree |
| Builder | An object has many optional parts | Requests, configurations |
| Singleton | Exactly 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
- Lead with the problem, then name the pattern: "fees will change by vehicle and time, so the fee calculation is a strategy".
- Don't force patterns. A pattern without the problem it solves is extra code. Interviewers notice pattern-stuffing.
- Singleton is a yellow flag in most interviews: it hides a dependency and makes tests share state. Prefer creating one instance and injecting it.
- Know the neighbours: strategy vs state (who chooses the next behaviour?), decorator vs proxy (adds behaviour vs controls access).
6. Remember it
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 verb | The pattern |
|---|---|
| Swap an algorithm | Strategy |
| Change behaviour as state changes | State |
| Notify others of an event | Observer |
| Create without naming the class | Factory |
| Wrap to add behaviour | Decorator |
Watch it on an example
- "Shipping cost depends on the option chosen" → swap → Strategy.
- "Log every payment without changing the gateway" → wrap → Decorator.
- "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
8. Practice exercise (25 minutes)
- Add a strategy (5 min). A Freight shipping cost in the code below.
- Add a decorator (10 min). A RetryingGateway that retries a failed charge once.
- 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?
A vending machine behaves differently when idle or holding money. Which pattern?
Email and SMS services must react when an order is paid, without the order knowing them. Which pattern?
Why is Singleton often discouraged in interviews?
10. Go deeper
| Resource | Time | Why |
|---|---|---|
| The pattern catalogue (refactoring.guru) | Reference | Every classic pattern with diagrams and code. |
| Module 05: Class and sequence diagrams | Next | How to draw these patterns on a whiteboard. |
| P07 and P08 | 90 min | State and chain of responsibility in full problems. |