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 real-world analogy: the kitchen utensil drawer

Imagine entering a commercial restaurant kitchen. The chef doesn't have one giant, multi-purpose tool that chops, whisks, blends, and bakes. Instead, they have a drawer filled with specialized tools.

  1. The Hand Tools (Strategy): You want to scrape a bowl. You grab a silicone spatula. You want to flip a burger. You swap it for a metal turner. The action of flipping/scraping is delegated to the tool, which fits a standard handle size (the interface).
  2. The Order Board (Observer): A new order ticket is pinned to the kitchen rail. The line cook starts cooking, the expeditor notes the table delay, and the busser readies the table. One event (new order) triggers updates to several decoupled listeners.
  3. The Kitchen Timer (State): A timer dial rotates. Depending on where the dial points (Waiting, Preheating, Baking, Done), pressing the start button does completely different things. You can't bake without preheating first—the behavior is bound to the timer's current state.

Our job in LLD is to avoid writing one giant, unmaintainable "all-in-one" utility class, and instead layout standard interfaces, behaviors, and lifecycles using established patterns.


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.

TypeScript

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.

Python

Python
from abc import ABC, abstractmethod

class PricingStrategy(ABC):
    @abstractmethod
    def price(self, base: float) -> float:
        pass

class RegularPricing(PricingStrategy):
    def price(self, base: float) -> float:
        return base

class MemberPricing(PricingStrategy):
    def price(self, base: float) -> float:
        return base * 0.9

class Checkout:
    def __init__(self, strategy: PricingStrategy):
        self.strategy = strategy
        
    def total(self, base: float) -> float:
        return self.strategy.price(base)

Java

Java
public interface PricingStrategy {
    double price(double base);
}

public class RegularPricing implements PricingStrategy {
    @Override
    public double price(double base) { return base; }
}

public class MemberPricing implements PricingStrategy {
    @Override
    public double price(double base) { return base * 0.9; }
}

public class Checkout {
    private final PricingStrategy strategy;
    
    public Checkout(PricingStrategy strategy) {
        this.strategy = strategy;
    }
    
    public double total(double base) {
        return this.strategy.price(base);
    }
}

C++

C++
#include <memory>

class PricingStrategy {
public:
    virtual ~PricingStrategy() = default;
    virtual double price(double base) const = 0;
};

class RegularPricing : public PricingStrategy {
public:
    double price(double base) const override { return base; }
};

class MemberPricing : public PricingStrategy {
public:
    double price(double base) const override { return base * 0.9; }
};

class Checkout {
private:
    std::shared_ptr<PricingStrategy> strategy;
public:
    Checkout(std::shared_ptr<PricingStrategy> strat) : strategy(strat) {}
    double total(double base) const { return strategy->price(base); }
};

Observer — events & notifications

Watch the fan-out. The subject keeps a subscriber list and, on every publish (or emit), calls the same update on each one—it never knows their concrete types or how many there are.

TypeScript

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);

Python

Python
from typing import Callable, Set

class PriceFeed:
    def __init__(self):
        self._listeners: Set[Callable[[float], None]] = set()
        
    def subscribe(self, callback: Callable[[float], None]) -> Callable[[], None]:
        self._listeners.add(callback)
        return lambda: self._listeners.remove(callback)
        
    def emit(self, price: float):
        for callback in list(self._listeners): # Copy list for concurrent safety
            callback(price)

Java

Java
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

public interface PriceListener {
    void onPriceUpdate(double price);
}

public class PriceFeed {
    private final Set<PriceListener> listeners = new CopyOnWriteArraySet<>();
    
    public void subscribe(PriceListener listener) {
        listeners.add(listener);
    }
    
    public void unsubscribe(PriceListener listener) {
        listeners.remove(listener);
    }
    
    public void emit(double price) {
        for (PriceListener listener : listeners) {
            listener.onPriceUpdate(price);
        }
    }
}

C++

C++
#include <vector>
#include <functional>
#include <algorithm>

class PriceFeed {
private:
    std::vector<std::function<void(double)>> listeners;
public:
    void subscribe(std::function<void(double)> callback) {
        listeners.push_back(callback);
    }
    
    void emit(double price) {
        for (const auto& cb : listeners) {
            cb(price);
        }
    }
};
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.

Step through a media player below. Watch how each event is handled by the current state.

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

TypeScript

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); }
}

Python

Python
from abc import ABC, abstractmethod

class PlayerState(ABC):
    @abstractmethod
    def play(self, player) -> None: pass
    @abstractmethod
    def stop(self, player) -> None: pass

class StoppedState(PlayerState):
    def play(self, player) -> None:
        player.state = PlayingState()
        print("Now playing.")
    def stop(self, player) -> None:
        print("Already stopped.")

class PlayingState(PlayerState):
    def play(self, player) -> None:
        print("Already playing.")
    def stop(self, player) -> None:
        player.state = StoppedState()
        print("Stopped.")

class Player:
    def __init__(self):
        self.state: PlayerState = StoppedState()
    def play(self):
        self.state.play(self)
    def stop(self):
        self.state.stop(self)

Java

Java
public interface PlayerState {
    void play(Player player);
    void stop(Player player);
}

public class StoppedState implements PlayerState {
    @Override
    public void play(Player player) {
        player.setState(new PlayingState());
        System.out.println("Now playing.");
    }
    @Override
    public void stop(Player player) {
        System.out.println("Already stopped.");
    }
}

public class PlayingState implements PlayerState {
    @Override
    public void play(Player player) {
        System.out.println("Already playing.");
    }
    @Override
    public void stop(Player player) {
        player.setState(new StoppedState());
        System.out.println("Stopped.");
    }
}

public class Player {
    private PlayerState state = new StoppedState();
    
    public void setState(PlayerState state) { this.state = state; }
    public void play() { state.play(this); }
    public void stop() { state.stop(this); }
}

C++

C++
#include <iostream>
#include <memory>

class Player;

class PlayerState {
public:
    virtual ~PlayerState() = default;
    virtual void play(Player& player) = 0;
    virtual void stop(Player& player) = 0;
};

class StoppedState : public PlayerState {
public:
    void play(Player& player) override;
    void stop(Player& player) override;
};

class PlayingState : public PlayerState {
public:
    void play(Player& player) override;
    void stop(Player& player) override;
};

class Player {
private:
    std::shared_ptr<PlayerState> state;
public:
    Player();
    void setState(std::shared_ptr<PlayerState> nextState) { state = nextState; }
    void play() { state->play(*this); }
    void stop() { state->stop(*this); }
};

// Implementations separated to avoid circular reference compiler issues
Player::Player() { state = std::make_shared<StoppedState>(); }
void StoppedState::play(Player& player) {
    player.setState(std::make_shared<PlayingState>());
    std::cout << "Now playing.\n";
}
void StoppedState::stop(Player& player) { std::cout << "Already stopped.\n"; }
void PlayingState::play(Player& player) { std::cout << "Already playing.\n"; }
void PlayingState::stop(Player& player) {
    player.setState(std::make_shared<StoppedState>());
    std::cout << "Stopped.\n";
}

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.


Interactive Quiz

Check yourself0/3 answered

1.

2.

3.


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.