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.
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?
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.
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.
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.
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.
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.
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.
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
Expose one interface (stack) backed by another (queue) — Adapter in three methods.
Core — Composite & Command
Trees of objects; reversible operations.Treat leaves and branches uniformly — Composite plus an Iterator.
- Design Browser HistoryMedium
Reversible forward/back operations over state — the Command / Memento shape.
Stretch — recursive structure behind a clean API
A recursive Composite (trie) hidden behind add / search — structure the caller never sees.