Design Patterns That Actually Come Up

The creational/structural/behavioral patterns worth knowing cold, each with its one-line intent and the situation that calls for it — plus Strategy and Observer in code.

design-patternsGoFbehavioral

The shortlist

CategoryPatternIntent (one line)
CreationalFactory Methodcreate objects without naming the concrete class
CreationalBuilderconstruct a complex object step by step
CreationalSingletonone shared instance (use sparingly)
StructuralAdaptermake an incompatible interface fit
StructuralDecoratoradd behavior by wrapping, not subclassing
StructuralFacadeone simple entry point over a messy subsystem
BehavioralStrategyswap an algorithm at runtime behind an interface
BehavioralObservernotify many subscribers when state changes
BehavioralStatebehavior 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.

Observer — publish / subscribe fan-outtime O(subscribers) per publishspace O(subscribers)
SubjectChartAlertsLogger

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.

subscribers = 0

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.

State pattern — media playertime O(1) per eventspace O(states)
playpauseplaystopstopStoppedPlayingPaused
events:playpauseplaystop

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.

state = Stopped
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.

Don't pattern-match for its own sake

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.

Practice ladder: Modeling state explicitly0/6 solved

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.
  1. Per-type counters — the seed of a state object.

  2. Encapsulate state behind a small, clear interface.

Core — explicit transitions

Draw the state diagram before you type.
  1. visit / back / forward — a state machine with a history pointer.

  2. Fixed-capacity state with full/empty guards on every transition.

Stretch — lifecycle & expiry

State that changes with time or paired events.
  1. Token lifecycle with TTL — state plus expiry.

  2. Check-in / check-out: paired state, aggregate on the closing transition.