The shortlist
| Category | Pattern | Intent (one line) |
|---|---|---|
| Creational | Factory Method | create objects without naming the concrete class |
| Creational | Builder | construct a complex object step by step |
| Creational | Singleton | one shared instance (use sparingly) |
| Structural | Adapter | make an incompatible interface fit |
| Structural | Decorator | add behavior by wrapping, not subclassing |
| Structural | Facade | one simple entry point over a messy subsystem |
| Behavioral | Strategy | swap an algorithm at runtime behind an interface |
| Behavioral | Observer | notify many subscribers when state changes |
| Behavioral | State | behavior changes with an internal state object |
How to pick — follow the force, not the name
Patterns are answers to recurring forces. Start from the problem you can feel in the code, and the pattern falls out:
In an interview, say the force out loud first ("seat-assignment policy varies → I'll put it behind a Strategy"), then name the pattern. Naming without the force is buzzword bingo; the force without the name still earns full credit.
Strategy — the most useful one
Encapsulate interchangeable algorithms behind a common interface and inject the
one you want. Kills if/else-on-type and satisfies Open/Closed.
interface PricingStrategy { price(base: number): number; }
const regular: PricingStrategy = { price: (b) => b };
const member: PricingStrategy = { price: (b) => b * 0.9 };
class Checkout {
constructor(private strategy: PricingStrategy) {}
total(base: number) { return this.strategy.price(base); }
}
// Add a BlackFridayStrategy later without touching Checkout.
Observer — events & notifications
type Listener<T> = (event: T) => void;
class Subject<T> {
private listeners = new Set<Listener<T>>();
subscribe(l: Listener<T>) { this.listeners.add(l); return () => this.listeners.delete(l); }
emit(event: T) { for (const l of this.listeners) l(event); }
}
// priceFeed.subscribe(updateChart); priceFeed.subscribe(checkAlerts);
Watch the fan-out. The subject keeps a subscriber list and, on every publish,
calls the same update on each one — it never knows their concrete types or how
many there are. Subscribe, publish, then unsubscribe one and publish again.
solid = subscribed · dashed = not listening · the subject calls the same method on every subscriber
1/15A subject and 3 possible observers. The subject holds a list of subscribers and knows nothing else about them — that decoupling is the whole pattern.
State — behavior that changes with the object's mode
When an object behaves differently depending on a status field, that logic
tends to sprawl into the same switch (status) copied across every method. The
State pattern gives each mode its own object that knows which events it
accepts and which state comes next — so an illegal move (pressing pause while
already Stopped) is simply refused, not a forgotten if.
Step through a media player below. Watch how each event is handled by the current state, and add an illegal event to the list — it gets ignored because that state has no transition for it.
1/5Start in Stopped. Each event is handled by the current state — the State pattern moves this branching out of one giant switch and into the state objects themselves.
interface PlayerState { play(p: Player): void; stop(p: Player): void; }
class Stopped implements PlayerState {
play(p: Player) { p.state = new Playing(); } // valid move
stop(_: Player) {} // already stopped — no-op
}
class Playing implements PlayerState {
play(_: Player) {} // already playing
stop(p: Player) { p.state = new Stopped(); }
}
class Player {
state: PlayerState = new Stopped();
play() { this.state.play(this); } // no switch — just delegate
stop() { this.state.stop(this); }
}
The Player has zero if (status === …) branches: each state encapsulates its
own transitions. Adding a Paused state is one new class, not an edit to five
methods — that's Open/Closed in action.
Naming a pattern is worthless if it doesn't fit. The signal is recognizing the forces — "the algorithm varies → Strategy", "many things react to a change → Observer", "objects behave differently per mode → State" — and knowing the cost (Singleton hides global state and wrecks testability; Decorator adds wrapper layers). Reach for the simplest thing that removes the duplication.
Singleton — handle with care
It guarantees one instance, but it's global mutable state in disguise: it hides dependencies, complicates tests, and is a footgun under concurrency (use lazy-init with proper locking or eager init). Often dependency injection of a single shared instance is the better answer.
Practice — model state & transitions
These are design problems, not algorithm puzzles. Each one is really "name the states and the events between them" — exactly the State pattern. Write the transition table first, then the code falls out.
Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.
Warm-up — state as plain fields
One object, a little internal state.Per-type counters — the seed of a state object.
- Design HashSetEasy
Encapsulate state behind a small, clear interface.
Core — explicit transitions
Draw the state diagram before you type.- Design Browser HistoryMedium
visit / back / forward — a state machine with a history pointer.
- Design Circular DequeMedium
Fixed-capacity state with full/empty guards on every transition.
Stretch — lifecycle & expiry
State that changes with time or paired events.Token lifecycle with TTL — state plus expiry.
Check-in / check-out: paired state, aggregate on the closing transition.