Design patterns overview¶
Intent¶
A design pattern is a named answer to a recurring force in object-oriented code: something will vary, and the code that does not vary should stop changing. This page is the index, not the lesson: which pattern answers which symptom, which draw the same picture for different reasons, and when to use none; each pattern page gives the diagram, a tested module and the Pythonic shortcut.
When to use and when not to¶
Reach for a pattern when
- You can name the axis of change before you name the pattern. "Pricing is what they will ask me to change" justifies a Strategy; "I might need it later" justifies nothing.
- A conditional on type or state is spreading. An
if/elifladder over vehicle types wants a Factory Method; the same ladder over ticket statuses wants State. - A dependency must be faked in a test: clocks, id generators, providers and stores go behind a small interface and arrive by injection.
- Two objects must stop knowing about each other: the floor should not import the display board (Observer); a vendor SDK should not leak into the checkout (Adapter).
Pattern-itis, and how to catch it in yourself
- Announcing patterns before requirements. "I will use Singleton, Factory, Strategy and Observer" in the first minute is the fastest way to lose the round.
- A Singleton for every manager. One instance built in
mainand injected behaves the same and stays testable; the parking lot rejects the Singleton on purpose. - A Strategy with one strategy, a Builder for three required fields, an Observer between two objects that could simply call each other: a class and an indirection that buy nothing until the second variant exists.
- A class diagram larger than the problem. Seven classes for tic-tac-toe is a warning sign; interviewers want to watch you stop.
Two questions decide: which follow-up does this pattern turn into a one-class change, and which test does it make easier? If either answer needs more than a sentence, leave it out and say why.
Structure¶
The catalogue by family: intent in a phrase, and the Python idiom that is often enough.
| Pattern | Intent | Python idiom |
|---|---|---|
| Creational | ||
| Singleton | one instance, global access | module-level instance; inject instead |
| Factory Method | defer the concrete class to a creator | dict registry, classmethod constructor |
| Abstract Factory | families of objects that must match | dataclass of callables; a module |
| Builder | stepwise construction, validated once | keyword-only args, replace |
| Prototype | clone a configured object | copy.deepcopy, dataclasses.replace |
| Structural | ||
| Adapter | fit a foreign interface | thin wrapper over a Protocol |
| Bridge | two hierarchies vary independently | injected implementor |
| Composite | part-whole trees, treated uniformly | recursive dataclasses, __iter__ |
| Decorator | add behaviour, stackable | @decorator, functools.wraps |
| Facade | one door into a subsystem | module-level function |
| Flyweight | share intrinsic state | interning, lru_cache, Enum |
| Proxy | control access to an object | __getattr__ delegation, cached_property |
| Behavioural | ||
| Chain of Responsibility | handlers tried in order | list of callables |
| Command | a request as an object | callables, partial, (do, undo) pairs |
| Interpreter | a grammar plus an evaluator | ast, match |
| Iterator | sequential access, structure hidden | generators, itertools |
| Mediator | centralise many-to-many talk | one coordinator object |
| Memento | snapshot and restore | frozen dataclass, deepcopy |
| Observer | one change, many listeners | callback list, signals |
| State | behaviour by lifecycle stage | Enum plus transition table |
| Strategy | interchangeable algorithms | callables, sorted(key=) |
| Template Method | fixed skeleton, varying steps | ABC hooks; pass a callable |
| Visitor | new operations over a structure | singledispatch, match |
| Enterprise | ||
| Repository | collection-like persistence | Protocol plus a dict-backed fake |
| Unit of Work | commit several changes atomically | contextmanager |
| Dependency Injection | collaborators supplied from outside | constructor args and Protocols |
| Null Object | a do-nothing stand-in | NullHandler, nullcontext |
| Specification | composable business rules | predicates, operator overloading |
| Object Pool | reuse expensive objects | queue.Queue plus context manager |
| Pipeline and Middleware | ordered stages that may short-circuit | reduce over callables |
| Event Bus | in-process pub/sub by topic | defaultdict(list) plus a worker |
Symptom to pattern: start from what will change.
flowchart LR
q_root["What will they ask you to change?"]
q_root --> q_make["How an object is created"]
q_root --> q_shape["How objects are composed"]
q_root --> q_act["How objects behave or talk"]
q_root --> q_infra["How the domain reaches storage and time"]
q_make -->|"one class, picked by a key"| p_factory["Factory Method"]
q_make -->|"a family that must stay consistent"| p_abstract["Abstract Factory"]
q_make -->|"many optional parts, validated once"| p_builder["Builder"]
q_make -->|"a copy of a configured object"| p_prototype["Prototype"]
q_shape -->|"a foreign interface"| p_adapter["Adapter"]
q_shape -->|"extra behaviour, stackable"| p_decorator["Decorator"]
q_shape -->|"controlled or lazy access"| p_proxy["Proxy"]
q_shape -->|"part-whole trees"| p_composite["Composite"]
q_shape -->|"one door into a subsystem"| p_facade["Facade"]
q_act -->|"the algorithm, chosen by the caller"| p_strategy["Strategy"]
q_act -->|"behaviour by lifecycle stage"| p_state["State"]
q_act -->|"one change, many listeners"| p_observer["Observer"]
q_act -->|"requests to queue, log or undo"| p_command["Command"]
q_act -->|"fixed skeleton, varying steps"| p_template["Template Method"]
q_act -->|"handlers tried in order"| p_chain["Chain of Responsibility"]
q_infra -->|"collection-like persistence"| p_repo["Repository"]
q_infra -->|"several changes, one commit"| p_uow["Unit of Work"]
q_infra -->|"fakes for clocks, ids and stores"| p_di["Dependency Injection"]
One shape, three intents: Decorator, Proxy and Adapter all hold a reference and forward a call.
classDiagram
class Sender {
<<interface>>
+send(notification: Notification) Receipt
}
class EmailSender {
+sender_address: str
+send(notification: Notification) Receipt
}
class RetryingSender {
-_inner: Sender
+attempts: int
+send(notification: Notification) Receipt
}
class LazySender {
-_inner: Sender
+send(notification: Notification) Receipt
}
class VendorSdk {
+post_message(payload: Dict~str,str~) str
}
class VendorAdapter {
-_sdk: VendorSdk
+send(notification: Notification) Receipt
}
Sender <|.. EmailSender
Sender <|.. RetryingSender : Decorator
Sender <|.. LazySender : Proxy
Sender <|.. VendorAdapter : Adapter
RetryingSender o-- Sender : wraps any sender
LazySender o-- Sender : builds one sender late
VendorAdapter o-- VendorSdk : translates calls to
The boxes are identical, so the answer has to be the intent: say what the wrapper adds (Decorator), withholds (Proxy) or translates (Adapter).
Related patterns and confusions¶
Four groups cover most "how is that different" follow-ups; each has one separating question.
| Confused group | The question that separates them |
|---|---|
| Strategy vs State vs Template Method | Who picks the behaviour, and when? Strategy: the caller injects one of several interchangeable algorithms that do not know each other. State: the object swaps its own behaviour as events move it through a lifecycle, and each state knows its successors. Template Method: a base class fixes the skeleton and subclasses fill in steps, decided when the class is written. |
| Decorator vs Proxy vs Adapter | Same interface or a different one, and what happens to the call? Decorator keeps the interface and adds behaviour; several can stack (retry around rate limit around logging). Proxy keeps the interface and controls access (lazy creation, caching, permissions, a remote stub), normally one per subject. Adapter changes the interface so an object you do not own fits the one your code expects. |
| Observer vs Mediator vs Event Bus | Who knows whom? Observer: the subject holds its observers and calls them directly, one subject to many listeners. Mediator: colleagues know only the mediator, which holds the interaction rules (the elevator controller). Event Bus: keyed by topic, both sides know only the bus, dispatch may be asynchronous and one failing handler does not stop the rest. |
| Facade vs Adapter | Simpler, or compatible? A Facade offers a new, smaller interface over several objects so the caller makes one call (place_order) instead of six. An Adapter offers an existing interface over one object so a client written against it keeps working. A facade may contain adapters; an adapter never simplifies. |
The creational trio: Factory Method picks one class by key, Abstract Factory a matching family, Builder assembles one object from many optional parts.
Where it appears in LLD problems¶
Every problem page has a "Design patterns applied" table with a why column; these recur most often.
| Problem | Patterns that earn their place |
|---|---|
| Design a parking lot | Strategy (pricing, allocation), Factory Method (vehicles), Observer (display); Singleton rejected for injection |
| Design an elevator system | State (car lifecycle), Strategy (dispatch), Mediator (controller), Command (requests) |
| Design a vending machine (and a coffee machine) | State (core), Decorator (add-ons), Strategy (change-making) |
| Design a text editor with undo and redo | Command (core), Memento (the alternative), Flyweight (styles) |
| Design a logging framework | Chain of Responsibility (propagation), Strategy (formatter), Builder (config), Null Object |
| Design a notification service (LLD) | Pipeline (preferences, dedup, rate limit), Strategy (senders, retries), Decorator (circuit breaker), Null Object |
| Design a payment gateway and digital wallet | Adapter (providers), Unit of Work (ledger), Chain of Responsibility (fraud), State (payment) |
Interview tips¶
Interview tip
Introduce a pattern as a consequence, never as a plan: "the pricing rule is what they will ask me to change, so it goes behind a PricingStrategy and the gate only ever calls price". One sentence of symptom, one of pattern, one naming the test it makes easy. Interviewers grade the justification, not the vocabulary.
Common mistake
Treating the Gang of Four list as a checklist and the Java shape as the only shape. A Strategy with one method is a function in Python, a Singleton is usually a module or an injected instance, a Builder is often keyword arguments with defaults. Draw the diagram the interviewer expects, then say which idiom you would actually write and why.
Related¶
- Problem to pattern quick reference — the same map as one table
- Strategy — learn it first; the model for every Pythonic variant
- State — Strategy's most frequent confusion
- Observer — the pattern behind every "notify" requirement
- The LLD interview framework — where "patterns only where justified" sits in the round
- Gamma, Helm, Johnson and Vlissides, Design Patterns (1994)
- Fowler, Patterns of Enterprise Application Architecture (2002), for the enterprise rows