Design Patterns in Depth

The second tier of must-know patterns — Factory, Builder, Adapter, Decorator, Composite, Command, Chain of Responsibility — each with the problem it kills and where you already use it.

LLDdesign-patternscreationalstructuralbehavioral

How to read this page

The shortlist doc covered the three patterns you'll defend in interviews (Strategy, Observer, Singleton). This page adds the seven that complete a working vocabulary — grouped the classic way: creational (how objects get made), structural (how objects compose), behavioral (how objects collaborate).

For each: the pain it removes, the shape, and where you've already used it without knowing. That last part is the real test — patterns you can spot in libraries are patterns you actually understand. A pattern named without a pain is a buzzword; lead with the pain.

Creational

Factory Method / Simple Factory — "don't make callers know class names"

Pain: code littered with new PdfExporter(), new CsvExporter() behind if (format == ...) chains — every new format edits every call site.

Python
class ExporterFactory:
    _registry = {"pdf": PdfExporter, "csv": CsvExporter}

    @classmethod
    def create(cls, fmt: str) -> Exporter:
        if fmt not in cls._registry:
            raise ValueError(f"unknown format: {fmt}")
        return cls._registry[fmt]()          # caller never names a class

exporter = ExporterFactory.create(request.format)
exporter.export(report)                      # polymorphism does the rest

Construction knowledge lives in one place; callers depend only on the Exporter interface (DIP). Adding a format = one registry entry. You've used it: json.loads deciding what to build, every DI container (Spring is a factory the size of a framework), datetime.fromisoformat.

Builder — "constructors with nine arguments are unreadable"

Pain: Pizza(true, false, null, 12, null, true) — what do those mean? Which are optional?

Java
HttpRequest req = HttpRequest.newBuilder()       // you've seen this exact API
    .uri(URI.create("https://api.example.com/orders"))
    .timeout(Duration.ofSeconds(5))
    .header("Authorization", token)
    .POST(BodyPublishers.ofString(json))
    .build();                                    // validate once, emit immutable object

Each setter is named and optional; build() validates the combination and returns an immutable result. Java/C++ lean on it heavily; Python mostly doesn't need it — keyword arguments (Pizza(cheese=True, size=12)) solve the same pain natively, and saying that is an interview point, not a dodge.

Structural

Adapter — "right behavior, wrong interface"

Pain: your code expects PaymentGateway.charge(amount); the new provider's SDK exposes submit_transaction(cents, currency, ref). You can't change their SDK, and you shouldn't change your 40 call sites.

Python
class StripeAdapter(PaymentGateway):            # wraps theirs, speaks yours
    def __init__(self, sdk: StripeSDK):
        self._sdk = sdk

    def charge(self, amount: Money) -> Receipt:
        result = self._sdk.submit_transaction(
            cents=amount.to_cents(), currency=amount.currency, ref=new_ref()
        )
        return Receipt.from_stripe(result)       # translate the answer too

One translation layer; both sides stay unchanged. This is the pattern of integration work — every "write a wrapper for the vendor SDK" ticket is an Adapter. It's also your testing seam: a FakeGateway implements the same interface (the DI story).

Decorator — "add behavior without touching the class"

Pain: you need retries on the payment gateway. And logging. And metrics. Subclassing gives RetryingLoggingMeteredGateway — a combinatorial explosion.

Python
class RetryingGateway(PaymentGateway):          # wraps ANY gateway
    def __init__(self, inner: PaymentGateway, attempts=3):
        self._inner, self._attempts = inner, attempts

    def charge(self, amount):
        for i in range(self._attempts):
            try:
                return self._inner.charge(amount)     # delegate
            except TransientError:
                backoff(i)
        raise

gateway = MeteredGateway(RetryingGateway(StripeAdapter(sdk)))   # stack at runtime

The call passes through each layer like an onion — every wrapper speaks PaymentGateway on both sides, which is exactly why they stack:

Same interface in, same interface out — so decorators stack in any order, chosen at runtime. You've used it: Python's @lru_cache / @app.get decorators (the language feature is named after the pattern), Java's BufferedReader(new FileReader(...)), every middleware pipeline — middleware is Decorator applied to request handlers.

Adapter vs Decorator in one line (a favorite quiz): Adapter changes the interface, Decorator keeps the interface and adds behavior.

Composite — "treat one and many uniformly"

Pain: a folder contains files and folders; your size-calculator shouldn't care which it's holding.

Python
class Node(ABC):
    @abstractmethod
    def size(self) -> int: ...

class File(Node):
    def size(self): return self._bytes

class Folder(Node):
    def __init__(self): self.children: list[Node] = []
    def size(self): return sum(c.size() for c in self.children)   # recursion!

The tree shape from recursion and trees, expressed as classes: leaves and containers share one interface, and every operation becomes a traversal. You've used it: the DOM, UI component trees (React children), org charts, Splitwise groups containing groups.

Behavioral

Command — "turn an action into an object"

Pain: undo/redo, job queues, macros — anything where actions must be stored, not just executed.

Python
class Command(ABC):
    @abstractmethod
    def execute(self): ...
    @abstractmethod
    def undo(self): ...

class AddItemCommand(Command):
    def __init__(self, cart, item): self.cart, self.item = cart, item
    def execute(self): self.cart.add(self.item)
    def undo(self): self.cart.remove(self.item)

history.push(cmd); cmd.execute()        # undo = history.pop().undo()

Once an action is an object it can be queued (task queues carry serialized commands), logged (event sourcing is Command as a persistence strategy), replayed, or undone — the undo stack finally gets its element type.

Chain of Responsibility — "let a pipeline decide who handles it"

Pain: a request must pass auth, then rate limiting, then validation, then the handler — without one god-function knowing all steps.

Python
class Handler(ABC):
    def __init__(self): self._next = None
    def set_next(self, h): self._next = h; return h
    def handle(self, req):
        if self._next: return self._next.handle(req)

class AuthHandler(Handler):
    def handle(self, req):
        if not verify(req.token): raise Unauthorized()
        return super().handle(req)          # pass it on

Each link does one check and forwards; the chain is assembled at startup. You've used it: Express middleware is literally this pattern (the next() parameter is the chain), Spring's filter chain, logging frameworks' handler chains, ATM cash dispensing by denomination.

The honest meta-lesson

Patterns are vocabulary, not virtue. The failure mode interviews screen for isn't ignorance — it's pattern fever: a Builder for a two-field object, an AbstractFactoryFactory. Every pattern adds indirection, and indirection must pay rent (the simplicity rule). Strong answers name the pain first, the pattern second, and the simpler alternative they rejected third. ("Keyword args would do, but this is Java, so: Builder.")

Practice — level up

You learn a pattern by building its shape, not memorizing its name. Each drill below is one of the second-tier patterns in disguise — spot it as you solve.

Practice ladder: Patterns in practice0/4 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 — Adapter

  1. Expose one interface (stack) backed by another (queue) — Adapter in three methods.

Core — Composite & Command

Trees of objects; reversible operations.
  1. Treat leaves and branches uniformly — Composite plus an Iterator.

  2. Reversible forward/back operations over state — the Command / Memento shape.

Stretch — recursive structure behind a clean API

  1. A recursive Composite (trie) hidden behind add / search — structure the caller never sees.