Design Patterns Handbook
A practical catalog of 37 reusable design patterns. Each entry explains the design intent, identifies a good fit, and includes a worked implementation followed by one detailed, concrete usage scenario in every programming language covered by Dev Handbooks.
Patterns are vocabulary for recurring design pressures, not requirements. Start with the simplest design and introduce a pattern when its trade-off solves a problem you actually have.
Creational Patterns
Singleton
Guarantees that a type has one shared instance and provides a single access point to it.
How it works
The type controls construction and stores its shared instance in static or module-level state. Callers obtain that instance through a well-known accessor, which can create it lazily or initialize it eagerly before concurrent work begins.
When to use it
Use for process-wide coordination such as configuration or a hardware registry. Avoid it when ordinary dependency injection can make ownership and tests clearer.
Trade-offs
The shared lifetime is convenient, but it also introduces hidden global state and can make tests influence one another. Thread safety, initialization failures, and cleanup must be designed explicitly.
# Implementation
class AppConfig:
_instance = None
@classmethod
def instance(cls):
cls._instance = cls._instance or cls()
return cls._instance
def get_database_url(self):
return "database://localhost"
# Usage
def main():
app_config = AppConfig.instance()
print(app_config.get_database_url()) # => database://localhost
def open_connection(config):
return f"connected to {config.get_database_url()}"
same_config = AppConfig.instance()
assert same_config is app_config
connection = open_connection(same_config)
if __name__ == "__main__":
main()Factory Method
Moves object creation behind an overridable method so subclasses or implementations decide which concrete product to return.
How it works
A creator defines the point where a product is constructed, while a subclass, callback, or implementation supplies the concrete product. The surrounding workflow depends only on the product interface and can therefore stay unchanged as new products are added.
When to use it
Use when a framework knows when an object is needed but lets an extension choose its concrete type.
Trade-offs
Creation becomes extensible and easier to test, but the extra abstraction can produce many small creator and product types. A simple constructor or function is usually clearer when creation does not vary.
# Implementation
class HtmlButton:
def run(self):
return "<button>Save</button>"
class DialogFactory:
def render(self):
return HtmlButton()
# Usage
def main():
dialog_factory = DialogFactory()
product = dialog_factory.render()
print(product.run()) # => <button>Save</button>
def execute(factory):
product = factory.render()
return product.run()
first_result = execute(DialogFactory())
second_result = execute(DialogFactory())
if __name__ == "__main__":
main()Abstract Factory
Creates families of related objects through one interface without exposing their concrete classes.
How it works
The factory exposes several creation operations, one for each member of a product family. A concrete factory implements all operations for one variant, ensuring that clients receive mutually compatible products without naming their classes.
When to use it
Use when products must be compatible as a family, such as light and dark UI controls or database-specific components.
Trade-offs
Switching an entire product family becomes straightforward, but adding a new product kind changes every concrete factory. It works best when families vary more often than the set of products.
# Implementation
class DarkButton:
def run(self):
return "dark button rendered"
class UiFactory:
def create_button(self):
return DarkButton()
# Usage
def main():
ui_factory = UiFactory()
product = ui_factory.create_button()
print(product.run()) # => dark button rendered
def execute(factory):
product = factory.create_button()
return product.run()
first_result = execute(UiFactory())
second_result = execute(UiFactory())
if __name__ == "__main__":
main()Builder
Constructs a complex object step by step while keeping construction separate from the final representation.
How it works
A builder accumulates construction choices through a sequence of named steps and creates the final object only when build is called. Validation and defaults can live in the builder, while the resulting object can remain immutable and fully initialized.
When to use it
Use for objects with many optional fields, validation rules, or multiple representations.
Trade-offs
Builders make complex construction readable and prevent telescoping constructors, but they duplicate some of the product’s fields and add ceremony. They are unnecessary for objects with only a few required arguments.
# Implementation
class RequestBuilder:
def __init__(self):
self._parts = {}
def with_value(self, key, value):
self._parts[key] = value
return self
def build(self):
return dict(self._parts)
# Usage
def main():
request = RequestBuilder().with_value("method", "POST").build()
get_request = (
RequestBuilder()
.with_value("method", "GET")
.with_value("path", "/orders")
.build()
)
assert request["method"] == "POST"
assert get_request["path"] == "/orders"
if __name__ == "__main__":
main()Prototype
Creates objects by copying an existing prototype instead of rebuilding them from scratch.
How it works
A configured object acts as a template and exposes a cloning operation. Clients copy the template and modify only the state that differs, with the implementation deciding whether nested mutable objects require a shallow or deep copy.
When to use it
Use when initialization is expensive or runtime configuration determines the object to copy.
Trade-offs
Cloning can avoid expensive setup and large constructor APIs, but copy semantics are easy to misunderstand. References, identity fields, external resources, and cyclic object graphs need deliberate handling.
# Implementation
from copy import copy
class ReportTemplate:
def __init__(self, title):
self.title = title
def clone(self):
return copy(self)
def customize(self):
self.title = "Quarterly report"
# Usage
def main():
report = ReportTemplate("Template").clone()
report.customize()
second_report = report.clone()
second_report.title = "Annual report"
assert report.title == "Quarterly report"
assert second_report.title == "Annual report"
assert report is not second_report
if __name__ == "__main__":
main()Object Pool
Keeps a bounded collection of reusable objects and lends them to clients for a limited time.
How it works
The pool creates and owns a bounded set of reusable objects. Acquire temporarily transfers one object to a client, and release resets and returns it so another client can reuse the same expensive resource.
When to use it
Use for expensive, limited resources such as database connections, worker processes, or large buffers.
Trade-offs
Pooling can control scarce resources and reduce allocation cost, but it introduces contention, waiting, reset rules, and leak risks. Modern runtimes often allocate ordinary objects cheaply, so pooling should be driven by measurement or a real resource limit.
# Implementation
class ConnectionPool:
def __init__(self):
self._items = {}
def acquire(self, key="default"):
return self._items.setdefault(key, {"key": key})
# Usage
def main():
connection_pool = ConnectionPool()
first = connection_pool.acquire("primary")
same = connection_pool.acquire("primary")
primary = connection_pool.acquire("primary")
same_primary = connection_pool.acquire("primary")
secondary = connection_pool.acquire("secondary")
assert primary is same_primary
assert primary is not secondary
if __name__ == "__main__":
main()Structural Patterns
Adapter
Converts one interface into another interface that an existing client expects.
How it works
The adapter implements the interface expected by the client and translates each call into the interface offered by an incompatible component. Translation may include renaming operations, reshaping data, converting errors, or coordinating several lower-level calls.
When to use it
Use to integrate legacy code or a third-party API without leaking its interface throughout the application.
Trade-offs
The incompatibility remains isolated at one boundary, but the adapter can become a misleading abstraction if the two models have genuinely different semantics. Keep translation explicit rather than silently discarding information.
# Implementation
class PaymentAdapter:
def __init__(self, target):
self._target = target
def pay(self):
result = self._target()
return f"Adapter: {result}"
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "legacy gateway charged 49.90 EUR"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "receipt stored"
# Usage
def main():
payment_adapter = PaymentAdapter(lambda: "completed")
print(payment_adapter.pay()) # => Adapter: completed
timed = PaymentAdapter(lambda: payment_adapter.pay())
first = payment_adapter.pay()
second = timed.pay()
assert "completed" in first
assert "completed" in second
if __name__ == "__main__":
main()Bridge
Separates an abstraction from its implementation so both dimensions can evolve independently.
How it works
The abstraction owns a reference to an implementation interface and delegates platform- or mechanism-specific work to it. New abstractions and new implementations can then be combined without creating a subclass for every possible pairing.
When to use it
Use when two independent axes of variation would otherwise create a large subclass matrix.
Trade-offs
Bridge avoids inheritance explosions and allows runtime composition, but it introduces another indirection and two related hierarchies. Apply it when both dimensions really change independently.
# Implementation
class RemoteControl:
def __init__(self, target):
self._target = target
def turn_on(self):
result = self._target()
return f"Bridge: {result}"
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "television powered on"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "radio powered on"
# Usage
def main():
remote_control = RemoteControl(lambda: "completed")
print(remote_control.turn_on()) # => Bridge: completed
timed = RemoteControl(lambda: remote_control.turn_on())
first = remote_control.turn_on()
second = timed.turn_on()
assert "completed" in first
assert "completed" in second
if __name__ == "__main__":
main()Composite
Arranges objects into trees and lets clients treat individual leaves and composed groups uniformly.
How it works
Leaves and containers implement the same component interface. A container stores child components and implements an operation by forwarding it recursively, allowing a complete tree and a single leaf to be used through the same API.
When to use it
Use for recursive part-whole structures such as menus, file trees, UI components, or organization charts.
Trade-offs
Recursive structures become easy to traverse and extend, but a very broad common interface can expose operations that do not make sense for every leaf. Parent ownership, cycles, and mutation rules also need clear definitions.
# Implementation
class MenuGroup:
def __init__(self):
self._steps = []
def add(self, step):
self._steps.append(step)
return self
def render(self, value="request"):
for step in self._steps:
value = step(value)
return value
# Usage
def main():
menu_group = MenuGroup().add(lambda value: value.upper())
result = menu_group.render()
menu_group.add(lambda value: f"[{value}]")
first = menu_group.render("first")
second = menu_group.render("second")
assert first == "[FIRST]"
assert second == "[SECOND]"
if __name__ == "__main__":
main()Decorator
Adds behavior by wrapping an object that implements the same interface, allowing features to be stacked dynamically.
How it works
A decorator implements the same contract as the wrapped component and keeps a reference to it. It performs work before or after delegation, and multiple decorators can be nested to assemble behavior at runtime.
When to use it
Use to add logging, caching, authorization, compression, or metrics without multiplying subclasses.
Trade-offs
Features remain small and composable, but a heavily decorated object can be difficult to inspect and debug. Ordering matters when decorators affect caching, authorization, retries, or transactions.
# Implementation
class CachedRepository:
def __init__(self, target):
self._target = target
def find_user(self):
result = self._target()
return f"Decorator: {result}"
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "database result for user-42"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "cached result for user-42"
# Usage
def main():
cached_repository = CachedRepository(lambda: "completed")
print(cached_repository.find_user()) # => Decorator: completed
timed = CachedRepository(lambda: cached_repository.find_user())
first = cached_repository.find_user()
second = timed.find_user()
assert "completed" in first
assert "completed" in second
if __name__ == "__main__":
main()Facade
Provides a small, cohesive interface over a larger or more complicated subsystem.
How it works
The facade exposes a use-case-oriented API and coordinates the lower-level objects required to complete it. The subsystem remains accessible when advanced callers need it, while ordinary clients depend on a smaller and more stable surface.
When to use it
Use at subsystem boundaries to give callers a stable entry point and hide orchestration details.
Trade-offs
A facade reduces coupling and simplifies common workflows, but it can grow into an oversized god object if unrelated use cases accumulate there. It should coordinate the subsystem rather than absorb all domain logic.
# Implementation
class CheckoutFacade:
def __init__(self, target):
self._target = target
def place_order(self):
result = self._target()
return f"Facade: {result}"
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "payment captured and shipment booked"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "confirmation sent"
# Usage
def main():
checkout_facade = CheckoutFacade(lambda: "completed")
print(checkout_facade.place_order()) # => Facade: completed
timed = CheckoutFacade(lambda: checkout_facade.place_order())
first = checkout_facade.place_order()
second = timed.place_order()
assert "completed" in first
assert "completed" in second
if __name__ == "__main__":
main()Flyweight
Shares immutable intrinsic state between many small objects while keeping context-specific state outside them.
How it works
A factory identifies shareable intrinsic state with a key and returns an existing immutable flyweight when possible. Per-use extrinsic state is supplied by the caller instead of being stored in every repeated object.
When to use it
Use when an application creates huge numbers of similar objects and duplicated state dominates memory.
Trade-offs
Large object populations can use far less memory, but callers must separate intrinsic and extrinsic state correctly. Lookup cost and a long-lived cache can outweigh the savings for small populations.
# Implementation
class GlyphFactory:
def __init__(self):
self._items = {}
def get_glyph(self, key="default"):
return self._items.setdefault(key, {"key": key})
# Usage
def main():
glyph_factory = GlyphFactory()
first = glyph_factory.get_glyph("primary")
same = glyph_factory.get_glyph("primary")
primary = glyph_factory.get_glyph("primary")
same_primary = glyph_factory.get_glyph("primary")
secondary = glyph_factory.get_glyph("secondary")
assert primary is same_primary
assert primary is not secondary
if __name__ == "__main__":
main()Proxy
Places a stand-in in front of another object to control, defer, or monitor access to it.
How it works
The proxy implements the subject interface and holds or locates the real subject. It intercepts calls to perform access checks, lazy initialization, network transport, caching, or instrumentation before forwarding the request.
When to use it
Use for lazy loading, remote calls, access control, caching, or instrumentation.
Trade-offs
Cross-cutting access behavior stays transparent to clients, but transparency can hide latency, remote failure, or expensive initialization. APIs should still communicate important operational differences.
# Implementation
class SecureDocumentProxy:
def __init__(self, target):
self._target = target
def read(self):
result = self._target()
return f"Proxy: {result}"
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "document loaded"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "access audited"
# Usage
def main():
secure_document_proxy = SecureDocumentProxy(lambda: "completed")
print(secure_document_proxy.read()) # => Proxy: completed
timed = SecureDocumentProxy(lambda: secure_document_proxy.read())
first = secure_document_proxy.read()
second = timed.read()
assert "completed" in first
assert "completed" in second
if __name__ == "__main__":
main()Behavioral Patterns
Chain of Responsibility
Passes a request through an ordered chain of handlers until one handles it or the chain ends.
How it works
Handlers share a common contract and are linked or stored in an ordered pipeline. Each handler can process the request, stop propagation, or pass a transformed request to the next handler without knowing the rest of the chain.
When to use it
Use when several independent policies may process a request, as in middleware, validation, or approval flows.
Trade-offs
Handlers are independently reusable and easy to reorder, but control flow becomes distributed across the chain. Requests may go unhandled, and debugging requires visibility into ordering and short-circuit decisions.
# Implementation
class RequestPipeline:
def __init__(self):
self._steps = []
def add(self, step):
self._steps.append(step)
return self
def handle(self, value="request"):
for step in self._steps:
value = step(value)
return value
# Usage
def main():
request_pipeline = RequestPipeline().add(lambda value: value.upper())
result = request_pipeline.handle()
request_pipeline.add(lambda value: f"[{value}]")
first = request_pipeline.handle("first")
second = request_pipeline.handle("second")
assert first == "[FIRST]"
assert second == "[SECOND]"
if __name__ == "__main__":
main()Command
Encapsulates a request as an object so it can be queued, logged, retried, composed, or undone.
How it works
A command object stores everything required to invoke an action, usually including its receiver and arguments. An invoker can execute commands immediately or treat them as data for queues, history, retries, scheduling, and undo.
When to use it
Use for job queues, menus, transactional actions, macros, and undoable operations.
Trade-offs
Commands separate the requester from execution and enable powerful infrastructure, but they introduce a type or object for each action. Undo also requires capturing enough prior state to reverse an operation safely.
# Implementation
class SaveCommand:
def __init__(self, behavior):
self._behavior = behavior
def execute(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "document saved"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "save added to undo history"
# Usage
def main():
save_command = SaveCommand(PrimaryBehavior())
result = save_command.execute("quarterly-report.md")
primary = SaveCommand(PrimaryBehavior())
alternative = SaveCommand(AlternateBehavior())
first = primary.execute("quarterly-report.md")
second = alternative.execute("quarterly-report.md")
assert first != second
print(first) # => document saved
print(second) # => save added to undo history
if __name__ == "__main__":
main()Interpreter
Represents a small grammar as objects and evaluates sentences by walking that representation.
How it works
Grammar rules are represented as expression objects with a shared evaluation operation. Complex expressions compose terminal and non-terminal nodes into a syntax tree that interprets itself against an input context.
When to use it
Use for small, stable domain languages such as filters, validation rules, or simple configuration expressions.
Trade-offs
The object structure mirrors a small grammar and is easy to extend one rule at a time, but performance and complexity degrade as the language grows. Parser generators or established parsing libraries are better for substantial languages.
# Implementation
class RuleExpression:
def __init__(self, behavior):
self._behavior = behavior
def evaluate(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "rule matched"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "rule rejected"
# Usage
def main():
rule_expression = RuleExpression(PrimaryBehavior())
result = rule_expression.evaluate("age >= 18 AND country = FR")
primary = RuleExpression(PrimaryBehavior())
alternative = RuleExpression(AlternateBehavior())
first = primary.evaluate("age >= 18 AND country = FR")
second = alternative.evaluate("age >= 18 AND country = FR")
assert first != second
print(first) # => rule matched
print(second) # => rule rejected
if __name__ == "__main__":
main()Iterator
Traverses a collection without exposing its internal representation.
How it works
The iterator stores traversal position separately from the collection and exposes an operation for obtaining the next element. Different iterators can traverse the same structure lazily, in different orders, or with filtering applied.
When to use it
Use to provide a uniform traversal API or support lazy, filtered, or custom iteration order.
Trade-offs
Clients no longer depend on collection internals, but mutation during iteration requires a defined policy. Stateful iterators also have lifetimes and cannot always be reused or shared safely.
# Implementation
class OrderIterator:
def __init__(self, behavior):
self._behavior = behavior
def next(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "order-42"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "order-43"
# Usage
def main():
order_iterator = OrderIterator(PrimaryBehavior())
result = order_iterator.next("order queue")
primary = OrderIterator(PrimaryBehavior())
alternative = OrderIterator(AlternateBehavior())
first = primary.next("order queue")
second = alternative.next("order queue")
assert first != second
print(first) # => order-42
print(second) # => order-43
if __name__ == "__main__":
main()Mediator
Centralizes communication between collaborating objects so they do not depend directly on one another.
How it works
Colleague objects send interaction requests to a mediator instead of calling one another directly. The mediator knows the collaboration rules and decides which colleagues to notify or update for each interaction.
When to use it
Use when a network of peer-to-peer dependencies becomes difficult to change or reason about.
Trade-offs
Peer objects become simpler and less coupled, but coordination complexity moves into the mediator. Split mediators by workflow or bounded context before one central mediator becomes a new god object.
# Implementation
class ChatMediator:
def __init__(self):
self._steps = []
def add(self, step):
self._steps.append(step)
return self
def send(self, value="request"):
for step in self._steps:
value = step(value)
return value
# Usage
def main():
chat_mediator = ChatMediator().add(lambda value: value.upper())
result = chat_mediator.send()
chat_mediator.add(lambda value: f"[{value}]")
first = chat_mediator.send("first")
second = chat_mediator.send("second")
assert first == "[FIRST]"
assert second == "[SECOND]"
if __name__ == "__main__":
main()Memento
Captures and restores an object’s internal state without exposing that state to its caretaker.
How it works
The originator creates an opaque snapshot containing the state needed for restoration. A caretaker stores snapshots without interpreting them and later gives one back to the originator to restore an earlier state.
When to use it
Use for undo, checkpoints, snapshots, and rollback of in-memory state.
Trade-offs
Encapsulation is preserved while supporting undo and checkpoints, but snapshots may consume substantial memory or retain sensitive data. Version compatibility matters if mementos are persisted.
# Implementation
class EditorHistory:
def __init__(self):
self._changes = []
def add(self, change):
self._changes.append(change)
def restore(self):
result = list(self._changes)
self._changes.clear()
return result
# Usage
def main():
editor_history = EditorHistory()
editor_history.add("order-created")
result = editor_history.restore()
editor_history.add("order-paid")
editor_history.add("order-shipped")
snapshot = editor_history.restore()
assert "order-created" in snapshot
assert len(snapshot) == 3
if __name__ == "__main__":
main()Observer
Maintains a one-to-many subscription so observers are notified when a subject changes.
How it works
A subject maintains a collection of observer callbacks or objects. When its state changes, it iterates over a stable view of that collection and notifies each observer according to defined ordering and error-handling rules.
When to use it
Use for domain events, UI updates, notifications, and other loosely coupled reactions.
Trade-offs
Producers and reactions are loosely coupled, but notification order, re-entrant updates, unsubscribe behavior, and observer failures can create subtle bugs. Long-lived subjects can also retain observers accidentally.
# Implementation
class EventPublisher:
def __init__(self):
self._observers = []
def subscribe(self, observer):
self._observers.append(observer)
def notify(self, event):
for observer in self._observers:
observer(event)
# Usage
def main():
publisher = EventPublisher()
publisher.subscribe(lambda event: print(event)) # => order-created, then order-paid, then order-shipped
publisher.notify("order-created")
received = []
publisher.subscribe(received.append)
publisher.notify("order-paid")
publisher.notify("order-shipped")
assert received == ["order-paid", "order-shipped"]
if __name__ == "__main__":
main()State
Moves state-specific behavior into separate objects so an object changes behavior when its state changes.
How it works
The context delegates state-dependent operations to an object representing its current state. State objects perform behavior and may transition the context to another state, replacing large conditional blocks with explicit lifecycle components.
When to use it
Use when conditionals over lifecycle state are spreading across many methods.
Trade-offs
Transitions and permitted behavior become easier to isolate and test, but the design creates more types and can scatter the overall lifecycle. A simple enum and switch may be clearer for small state machines.
# Implementation
class OrderContext:
def __init__(self, behavior):
self._behavior = behavior
def advance(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "pending -> paid"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "paid -> shipped"
# Usage
def main():
order_context = OrderContext(PrimaryBehavior())
result = order_context.advance("order-42")
primary = OrderContext(PrimaryBehavior())
alternative = OrderContext(AlternateBehavior())
first = primary.advance("order-42")
second = alternative.advance("order-42")
assert first != second
print(first) # => pending -> paid
print(second) # => paid -> shipped
if __name__ == "__main__":
main()Strategy
Encapsulates interchangeable algorithms behind one interface and selects one at runtime.
How it works
The context accepts an object or function implementing an algorithm contract and delegates the variable part of its work to it. Strategies can be selected through configuration, user choice, or runtime conditions without changing the context.
When to use it
Use when behavior varies independently from the client, such as pricing, routing, sorting, or serialization.
Trade-offs
Algorithms become independently testable and replaceable, but clients or configuration must understand which strategy is appropriate. Too many tiny strategies can obscure logic that would be clearer inline.
# Implementation
class PriceCalculator:
def __init__(self, behavior):
self._behavior = behavior
def calculate(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "regular price: 100.00"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "VIP price: 80.00"
# Usage
def main():
price_calculator = PriceCalculator(PrimaryBehavior())
result = price_calculator.calculate("100.00")
primary = PriceCalculator(PrimaryBehavior())
alternative = PriceCalculator(AlternateBehavior())
first = primary.calculate("100.00")
second = alternative.calculate("100.00")
assert first != second
print(first) # => regular price: 100.00
print(second) # => VIP price: 80.00
if __name__ == "__main__":
main()Template Method
Defines an algorithm skeleton in a base type while allowing selected steps to be customized.
How it works
A base type implements the invariant workflow as a final sequence of steps. Subclasses override selected primitive operations or hooks while the base type retains control over ordering and mandatory behavior.
When to use it
Use when workflows share a fixed sequence but differ in a few well-defined steps.
Trade-offs
Shared workflow logic stays in one place, but customization relies on inheritance and is fixed at construction time. Strategy or composition is usually more flexible when steps must change dynamically.
# Implementation
class ImportJob:
def __init__(self, behavior):
self._behavior = behavior
def run(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "CSV imported: 120 rows"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "JSON imported: 120 rows"
# Usage
def main():
import_job = ImportJob(PrimaryBehavior())
result = import_job.run("customers.csv")
primary = ImportJob(PrimaryBehavior())
alternative = ImportJob(AlternateBehavior())
first = primary.run("customers.csv")
second = alternative.run("customers.csv")
assert first != second
print(first) # => CSV imported: 120 rows
print(second) # => JSON imported: 120 rows
if __name__ == "__main__":
main()Visitor
Adds operations to a stable object structure without putting every operation on the element types.
How it works
Each element accepts a visitor and calls the visitor operation specialized for its concrete type, producing double dispatch. A new visitor can implement an operation across the entire element hierarchy without modifying the element classes.
When to use it
Use when element types change rarely but new cross-cutting operations are added often.
Trade-offs
Adding cross-cutting operations is easy, but adding a new element type requires changing every visitor. Visitors can also weaken encapsulation by requiring elements to expose internal data.
# Implementation
class TaxVisitor:
def __init__(self, behavior):
self._behavior = behavior
def visit(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "book tax: 1.50"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "food tax: 0.00"
# Usage
def main():
tax_visitor = TaxVisitor(PrimaryBehavior())
result = tax_visitor.visit("book: 30.00")
primary = TaxVisitor(PrimaryBehavior())
alternative = TaxVisitor(AlternateBehavior())
first = primary.visit("book: 30.00")
second = alternative.visit("book: 30.00")
assert first != second
print(first) # => book tax: 1.50
print(second) # => food tax: 0.00
if __name__ == "__main__":
main()Null Object
Supplies a do-nothing implementation of an interface instead of representing missing behavior with null.
How it works
A null object implements the same interface as a real collaborator but performs neutral behavior, such as discarding log messages or returning an empty result. Clients receive a valid object and call it unconditionally.
When to use it
Use when optional collaborators would otherwise require repetitive null checks.
Trade-offs
Client code loses repetitive absence checks, but a silent null object can hide missing configuration or unexpected state. Use a name and behavior that make intentional absence clear.
# Implementation
class NullLogger:
def log(self, message):
pass
# Usage
def main():
logger = NullLogger()
logger.log("saved")
class ConsoleLogger:
def log(self, message):
print(message) # => order saved
def save_order(logger):
logger.log("order saved")
save_order(NullLogger())
save_order(ConsoleLogger())
if __name__ == "__main__":
main()Architectural & Enterprise Patterns
Repository
Presents domain objects through a collection-like interface and hides persistence details.
How it works
The repository translates between domain objects and a persistence mechanism while presenting operations such as find, add, and remove. Query construction and data mapping stay behind the boundary so application code speaks in domain terms.
When to use it
Use when domain logic should remain independent of SQL, HTTP, files, or an ORM.
Trade-offs
Persistence can be replaced and domain logic tested in isolation, but a repository that merely mirrors every ORM operation adds little value. Avoid leaking database-specific query objects through its interface.
# Implementation
from dataclasses import dataclass
@dataclass(frozen=True)
class User:
id: int
name: str
class UserRepository:
def __init__(self):
self._users = {}
def add(self, user):
self._users[user.id] = user
def find_by_id(self, user_id):
return self._users.get(user_id)
# Usage
def main():
repository = UserRepository()
repository.add(User(42, "Ada"))
user = repository.find_by_id(42)
repository.add(User(7, "Grace"))
ada = repository.find_by_id(42)
grace = repository.find_by_id(7)
missing = repository.find_by_id(999)
assert ada.name == "Ada"
assert grace.name == "Grace"
assert missing is None
if __name__ == "__main__":
main()Unit of Work
Tracks a set of changes and writes them as one coordinated transaction.
How it works
The unit tracks new, changed, and removed objects during a business transaction. Commit calculates the required persistence operations and applies them through one transaction, while rollback abandons or restores the tracked changes.
When to use it
Use when multiple repository operations must succeed or fail together.
Trade-offs
Atomic application workflows become explicit, but identity tracking, nested transactions, concurrency, and failure recovery are complex. Many ORMs already implement this pattern through their session or context.
# Implementation
class UnitOfWork:
def __init__(self):
self._changes = []
def add(self, change):
self._changes.append(change)
def commit(self):
result = list(self._changes)
self._changes.clear()
return result
# Usage
def main():
unit_of_work = UnitOfWork()
unit_of_work.add("order-created")
result = unit_of_work.commit()
unit_of_work.add("order-paid")
unit_of_work.add("order-shipped")
snapshot = unit_of_work.commit()
assert "order-created" in snapshot
assert len(snapshot) == 3
if __name__ == "__main__":
main()Dependency Injection
Supplies dependencies from outside an object instead of letting the object construct them itself.
How it works
An object declares its required collaborators as constructor or method parameters. A composition root creates concrete implementations and connects the object graph, manually or through a container, before application logic begins.
When to use it
Use to make dependencies explicit, swap implementations, and isolate units during testing.
Trade-offs
Dependencies become visible and tests can substitute focused fakes, but excessive interfaces and container configuration add indirection. Keep service location out of domain code so dependencies remain explicit.
# Implementation
class OrderService:
def __init__(self, behavior):
self._behavior = behavior
def place_order(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "order stored"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "confirmation emailed"
# Usage
def main():
order_service = OrderService(PrimaryBehavior())
result = order_service.place_order("order-42")
primary = OrderService(PrimaryBehavior())
alternative = OrderService(AlternateBehavior())
first = primary.place_order("order-42")
second = alternative.place_order("order-42")
assert first != second
print(first) # => order stored
print(second) # => confirmation emailed
if __name__ == "__main__":
main()Model–View–Controller
Separates domain state, presentation, and input coordination into model, view, and controller roles.
How it works
The model owns domain state and rules, the view renders a representation, and the controller translates incoming user or HTTP actions into model operations and view selection. Communication direction varies by platform, but responsibilities remain separated.
When to use it
Use for request-driven or interactive applications where presentation should evolve independently from domain logic.
Trade-offs
Presentation and domain behavior can evolve independently, but thin domain models often push too much logic into controllers. Define boundaries carefully because frameworks use the MVC names in different ways.
# Implementation
class UserController:
def __init__(self, behavior):
self._behavior = behavior
def show_profile(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "profile rendered for Ada"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "404 profile rendered"
# Usage
def main():
user_controller = UserController(PrimaryBehavior())
result = user_controller.show_profile("user-42")
primary = UserController(PrimaryBehavior())
alternative = UserController(AlternateBehavior())
first = primary.show_profile("user-42")
second = alternative.show_profile("user-42")
assert first != second
print(first) # => profile rendered for Ada
print(second) # => 404 profile rendered
if __name__ == "__main__":
main()Model–View–ViewModel
Exposes presentation-ready state and commands through a view model that a view can bind to.
How it works
The view model exposes observable, presentation-ready state plus commands representing user actions. The view binds to that surface, while the view model loads and transforms model data without depending on concrete UI controls.
When to use it
Use in reactive UIs where binding should keep rendering code thin and testable.
Trade-offs
Presentation logic becomes testable without rendering a UI, but binding systems can make update flow difficult to trace. Avoid duplicating the entire domain model as mutable view-model state.
# Implementation
class ProfileViewModel:
def __init__(self, behavior):
self._behavior = behavior
def load(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "display name: Ada Lovelace"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "loading: false"
# Usage
def main():
profile_view_model = ProfileViewModel(PrimaryBehavior())
result = profile_view_model.load("user-42")
primary = ProfileViewModel(PrimaryBehavior())
alternative = ProfileViewModel(AlternateBehavior())
first = primary.load("user-42")
second = alternative.load("user-42")
assert first != second
print(first) # => display name: Ada Lovelace
print(second) # => loading: false
if __name__ == "__main__":
main()Service Layer
Defines an application boundary that coordinates use cases, transactions, and domain objects.
How it works
A service-layer operation represents an application use case and coordinates repositories, domain objects, authorization, and transaction boundaries. Delivery adapters such as HTTP controllers, jobs, and command-line handlers call the same service API.
When to use it
Use when several delivery mechanisms must invoke the same application workflows.
Trade-offs
Use cases gain a stable boundary and avoid duplication across delivery channels, but services can become procedural collections of unrelated logic. Keep business invariants in the domain types that own them.
# Implementation
class TransferService:
def __init__(self, behavior):
self._behavior = behavior
def transfer(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "25.00 transferred from A to B"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "transaction committed"
# Usage
def main():
transfer_service = TransferService(PrimaryBehavior())
result = transfer_service.transfer("25.00 EUR")
primary = TransferService(PrimaryBehavior())
alternative = TransferService(AlternateBehavior())
first = primary.transfer("25.00 EUR")
second = alternative.transfer("25.00 EUR")
assert first != second
print(first) # => 25.00 transferred from A to B
print(second) # => transaction committed
if __name__ == "__main__":
main()Specification
Encapsulates a business rule as a composable, reusable predicate.
How it works
A specification gives a business predicate a domain name and an is-satisfied operation. Composite specifications combine rules with AND, OR, and NOT, and some implementations translate the same rule into a database query.
When to use it
Use when selection and validation rules need names, reuse, testing, and AND/OR/NOT composition.
Trade-offs
Complex rules become reusable and directly testable, but generic specification frameworks can become abstract and difficult to translate efficiently. Prefer explicit domain specifications over a universal expression system.
# Implementation
class EligibleCustomerSpec:
def __init__(self, behavior):
self._behavior = behavior
def is_satisfied_by(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "eligible: true"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "eligible: false"
# Usage
def main():
eligible_customer_spec = EligibleCustomerSpec(PrimaryBehavior())
result = eligible_customer_spec.is_satisfied_by("customer age 21, active")
primary = EligibleCustomerSpec(PrimaryBehavior())
alternative = EligibleCustomerSpec(AlternateBehavior())
first = primary.is_satisfied_by("customer age 21, active")
second = alternative.is_satisfied_by("customer age 21, active")
assert first != second
print(first) # => eligible: true
print(second) # => eligible: false
if __name__ == "__main__":
main()Event Sourcing
Stores an append-only sequence of domain events and rebuilds current state by replaying them.
How it works
Every accepted state change is recorded as an immutable event in an append-only stream. An aggregate rebuilds its state by replaying those events, and projections transform the stream into query-oriented read models.
When to use it
Use when audit history, temporal queries, or event-driven integration justifies the added operational complexity.
Trade-offs
The event history provides a strong audit trail and enables new projections, but schema evolution, replay cost, eventual consistency, and operational tooling are significant commitments. Events must describe durable business facts rather than transient implementation details.
# Implementation
class OrderAggregate:
def __init__(self):
self._changes = []
def add(self, change):
self._changes.append(change)
def apply(self):
result = list(self._changes)
self._changes.clear()
return result
# Usage
def main():
order_aggregate = OrderAggregate()
order_aggregate.add("order-created")
result = order_aggregate.apply()
order_aggregate.add("order-paid")
order_aggregate.add("order-shipped")
snapshot = order_aggregate.apply()
assert "order-created" in snapshot
assert len(snapshot) == 3
if __name__ == "__main__":
main()Event Bus
Routes named events from publishers to any interested subscribers without requiring either side to know about the other.
How it works
Subscribers register handlers under an event type or topic, and publishers send an event only to the bus. The bus looks up matching handlers and defines whether delivery is synchronous, queued, ordered, retried, or allowed to fail independently.
When to use it
Use for in-process integration between modules, plugin systems, UI events, or domain-event dispatch. Define ownership and delivery semantics carefully so event flow does not become invisible.
Trade-offs
Modules integrate without direct references, but control flow becomes less visible and event contracts become shared APIs. Duplicate delivery, handler ordering, backpressure, and error isolation must be explicit in non-trivial systems.
# Implementation
class EventBus:
def __init__(self):
self._handlers = {}
def subscribe(self, topic, handler):
self._handlers.setdefault(topic, []).append(handler)
def publish(self, topic, payload):
for handler in self._handlers.get(topic, []):
handler(payload)
# Usage
def main():
bus = EventBus()
bus.subscribe("order.created", lambda order: print(order)) # => order-42, then order-43
bus.publish("order.created", {"id": 42})
audit_log = []
bus.subscribe("order.created", audit_log.append)
bus.subscribe("order.cancelled", audit_log.append)
bus.publish("order.created", "order-43")
bus.publish("order.cancelled", "order-42")
assert audit_log == ["order-43", "order-42"]
if __name__ == "__main__":
main()CQRS
Separates models and paths for changing state from those used to query it.
How it works
Commands express intent to change state and are handled by a write model that protects invariants. Queries use a separate read model shaped for retrieval, which may be updated synchronously or asynchronously from write-side changes.
When to use it
Use when read and write workloads need different scaling, schemas, permissions, or consistency models.
Trade-offs
Each side can be modeled, secured, and scaled independently, but duplicated models and synchronization add complexity. CQRS does not require event sourcing and is often excessive for straightforward CRUD applications.
# Implementation
class OrderApplication:
def __init__(self, behavior):
self._behavior = behavior
def execute_command(self, value="input"):
return self._behavior(value)
class PrimaryBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "command accepted"
class AlternateBehavior:
def __call__(self, value):
if not value:
raise ValueError("value is required")
return "query returned order-42"
# Usage
def main():
order_application = OrderApplication(PrimaryBehavior())
result = order_application.execute_command("CreateOrder(order-42)")
primary = OrderApplication(PrimaryBehavior())
alternative = OrderApplication(AlternateBehavior())
first = primary.execute_command("CreateOrder(order-42)")
second = alternative.execute_command("CreateOrder(order-42)")
assert first != second
print(first) # => command accepted
print(second) # => query returned order-42
if __name__ == "__main__":
main()Saga
Coordinates a distributed business transaction as local steps with compensating actions for failures.
How it works
A saga breaks a business transaction into local transactions owned by participating services. After each successful step it advances, and after a later failure it invokes compensating actions for already completed steps, either through orchestration or events.
When to use it
Use across services when a single ACID transaction is unavailable and partial work must be undone explicitly.
Trade-offs
Long-running work can cross service boundaries without distributed locking, but compensation is not true rollback and intermediate states are visible. Idempotency, retries, timeouts, and durable progress tracking are essential.
# Implementation
class CheckoutSaga:
def __init__(self):
self._steps = []
def add(self, step):
self._steps.append(step)
return self
def start(self, value="request"):
for step in self._steps:
value = step(value)
return value
# Usage
def main():
checkout_saga = CheckoutSaga().add(lambda value: value.upper())
result = checkout_saga.start()
checkout_saga.add(lambda value: f"[{value}]")
first = checkout_saga.start("first")
second = checkout_saga.start("second")
assert first == "[FIRST]"
assert second == "[SECOND]"
if __name__ == "__main__":
main()Resilience & Resource Management Patterns
Circuit Breaker
Stops calls to a failing dependency temporarily, then probes it later to determine whether it recovered.
How it works
The breaker counts qualifying failures while closed and opens after a threshold, rejecting calls without contacting the dependency. After a cooldown it enters a half-open state, permits a limited probe, and closes again only after successful recovery.
When to use it
Use around remote dependencies to prevent cascading failures and give unhealthy services time to recover.
Trade-offs
Fail-fast behavior protects capacity and reduces cascading failure, but thresholds and timing require tuning. Pair the breaker with timeouts, bounded retries, monitoring, and a meaningful fallback where one exists.
# Implementation
class PaymentCircuitBreaker:
def __init__(self, target, limit=3):
self._target = target
self._failures = 0
self._limit = limit
def call(self):
if self._failures >= self._limit:
raise RuntimeError("circuit open")
try:
return self._target()
except Exception:
self._failures += 1
raise
# Usage
def main():
breaker = PaymentCircuitBreaker(lambda: "paid")
result = breaker.call()
def unavailable():
raise ConnectionError("payment service unavailable")
failing_breaker = PaymentCircuitBreaker(unavailable, limit=2)
errors = []
for attempt in range(3):
try:
failing_breaker.call()
except (ConnectionError, RuntimeError) as error:
errors.append(str(error))
assert failing_breaker._failures == 2
assert errors[-1] == "circuit open"
if __name__ == "__main__":
main()