Skip to content

Design Patterns

A design pattern names a recurring design trade-off. It is a vocabulary for discussing code, not a checklist of classes to implement. Use a pattern only after a concrete pressure appears; the language or standard library often already provides the simplest form.

Before Applying a Pattern

Ask, in order:

  1. Can the responsibility or variation be removed?
  2. Does an existing module already solve it?
  3. Does a function, value, built-in protocol, or composition suffice?
  4. Is the duplication actually harmful, or merely similar-looking code?
  5. Which requirement forces the extra indirection?
  6. How will the pattern make the next likely change safer?

Patterns add names, indirection, and extension points. That cost is justified when it isolates real variation, not hypothetical reuse.

Prefer Native Language Mechanisms

Many classic object-oriented patterns are small language features in Python:

Need Usually start with
Select behavior Function or dictionary of functions
Construct variants Class method or plain factory function
Adapt an interface Small wrapper function or object
Traverse values Iterator or generator
Add behavior around a call Decorator function or context manager
Notify listeners Callback list
Represent state variants Data plus explicit transition function
Delay expensive creation Cached property or memoization

Do not reproduce Java-shaped scaffolding when the language already expresses the intent directly.

Creation Patterns

Factory

A factory centralizes construction when the selected concrete type depends on input or configuration.

from dataclasses import dataclass


@dataclass
class JsonReport:
    compact: bool = False


@dataclass
class CsvReport:
    delimiter: str = ","


def make_report(kind: str):
    if kind == "json":
        return JsonReport()
    if kind == "csv":
        return CsvReport()
    raise ValueError(f"unsupported report type: {kind}")

Keep direct construction when there is only one implementation or the caller already knows the class.

Builder

A builder separates multi-step construction from the final value. It is useful for complex, validated objects or test data with many optional variations. Prefer keyword arguments, dataclasses, and small helper functions first.

Prototype

Prototype creates a value by copying an existing template. Use native shallow or deep copying only when copy semantics are clear; shared mutable members can make clones unsafe.

Singleton

A singleton enforces one instance within a process, not one resource across machines. Module-level objects and dependency injection are simpler. Global mutable singletons hide dependencies, leak state between tests, and do not provide distributed coordination.

Structural Patterns

Adapter

An adapter translates one interface into another at a boundary:

class LegacyPayments:
    def charge_cents(self, customer: str, cents: int) -> str:
        return "approved"


class PaymentAdapter:
    def __init__(self, legacy: LegacyPayments):
        self.legacy = legacy

    def charge(self, customer: str, amount: float) -> str:
        return self.legacy.charge_cents(customer, round(amount * 100))

Keep translation close to the dependency so its terminology and quirks do not spread through the application.

Facade

A facade exposes a small workflow-oriented interface over a complicated subsystem. It should simplify a boundary, not become a god object containing unrelated business logic.

Decorator

A decorator wraps behavior while preserving the same usable interface. It fits logging, metrics, authorization, caching, and retry when those behaviors are truly composable. Wrapper order matters, and hidden retries or caching can surprise callers.

Composite

A composite lets clients treat leaves and groups uniformly, as with files and directories or UI nodes. Use it only when operations genuinely apply to both; a forced common interface often accumulates invalid methods.

Proxy

A proxy controls access to another object for lazy loading, remote access, caching, or authorization. Unlike an adapter, it preserves the conceptual interface. Make network calls and other expensive behavior visible in naming or documentation.

Bridge

Bridge separates two independently varying dimensions. Before creating parallel class hierarchies, try composing two plain values or functions.

Flyweight

Flyweight shares immutable intrinsic state among many objects. Use it only after memory profiling shows repeated state is material; ordinary interning, caching, or normalized data may already solve the problem.

Behavioral Patterns

Strategy

Strategy makes an algorithm selectable. A function is usually enough:

from collections.abc import Callable


def total(prices: list[float], discount: Callable[[float], float]) -> float:
    return discount(sum(prices))


regular = lambda value: value
ten_percent_off = lambda value: value * 0.9

Use an object when a strategy owns meaningful state or several related operations.

Command

A command represents an action as data or an object. It is useful for queues, audit logs, scheduling, undo, and retries. Define idempotency and serialization when commands cross process boundaries.

Observer

Observers receive notifications when state changes. They decouple publishers from consumers but make control flow less visible. Define subscription lifetime, ordering, error isolation, reentrancy, and synchronous versus asynchronous delivery.

State

State moves behavior associated with lifecycle states out of scattered conditionals. Start with an enum and an explicit transition table; introduce state objects only when each state has substantial behavior.

TRANSITIONS = {
    ("draft", "submit"): "review",
    ("review", "approve"): "published",
    ("review", "reject"): "draft",
}


def transition(state: str, event: str) -> str:
    try:
        return TRANSITIONS[state, event]
    except KeyError as error:
        raise ValueError(f"invalid transition: {state} + {event}") from error

Chain of Responsibility

A chain passes a request through ordered handlers until one handles it or all contribute. Middleware and validation pipelines are common forms. Make ordering and short-circuit behavior explicit.

Template Method

A base class fixes an algorithm skeleton while subclasses override steps. Composition with injected functions is often easier to test and change than inheritance.

Iterator

An iterator exposes sequential access without revealing storage. Use the language's iterator and generator protocols; a custom hierarchy is rarely needed.

Mediator

A mediator coordinates interactions so peers do not all depend on one another. It can reduce a dependency mesh, but an oversized mediator merely relocates coupling.

Memento

A memento captures state for later restoration. Prefer immutable snapshots or an operation log, and define retention, privacy, and compatibility.

Visitor and Interpreter

Visitor adds operations across a stable family of node types; adding node types then becomes expensive. Interpreter represents and evaluates a small grammar. Both are specialized: pattern matching, plain recursion, or an existing parser is usually smaller.

Patterns at System Boundaries

Some patterns operate across processes and have failure semantics beyond class structure:

  • repository isolates domain code from persistence details;
  • dependency injection makes dependencies explicit at construction;
  • circuit breaker limits calls to a failing dependency;
  • outbox connects a database transaction to later message publication;
  • saga coordinates local transactions with compensating actions;
  • strangler fig replaces a legacy boundary incrementally.

See Software Architecture and Distributed Systems before applying distributed patterns.

Pattern Smells

  • an interface with one implementation and no test boundary;
  • a factory that only calls one constructor;
  • an abstract base class created “for later”;
  • a service locator or singleton hiding dependencies;
  • inheritance used only to share a few lines;
  • observers for a direct call between two known components;
  • a pattern name offered instead of a requirement;
  • many tiny objects that obscure one simple operation.

Review Checklist

  • What concrete variation or coupling does the pattern isolate?
  • Can a language feature or existing module do it with less machinery?
  • Is control flow still easy to trace?
  • Are ownership, lifetime, and failure behavior explicit?
  • Does the pattern improve the likely next change?
  • Can it be removed if the predicted variation never arrives?

The best pattern implementation is often much smaller than the textbook diagram.