Helpful context:


In 1977, an architect named Christopher Alexander published A Pattern Language. His subject was buildings and towns, but his method was unusual: he argued that good design arises from recurring solutions to recurring problems, and that these solutions could be named and described formally. A “pattern” in his sense was not a template to be copied - it was a distillation of hard-won wisdom about a recurring situation, available to be applied thoughtfully wherever that situation arose.

“Gateway,” “courtyards which live,” “entrance room” - Alexander’s patterns described forces in tension and the designs that resolved them. They transferred expertise from experienced architects to less experienced ones, in a form that could be reasoned about and discussed.

In 1994, four software engineers published Design Patterns: Elements of Reusable Object-Oriented Software. They were Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides - quickly nicknamed the Gang of Four (GoF). They had explicitly borrowed Alexander’s idea: name recurring solutions to recurring software design problems, describe the forces each one resolves, and give the field a shared vocabulary.

The book sold hundreds of thousands of copies. It shaped how an entire generation of engineers thought about software architecture. It also became the subject of one of the field’s most productive and revealing arguments.

What a Pattern Is (and Is Not)

A design pattern is not code. It is not a library. It is not something you import. It is a named solution structure for a class of recurring problems.

The Gang of Four organized their 23 patterns into three categories:

Creational - how are objects created? Structural - how are objects and classes composed? Behavioral - how do objects communicate and distribute responsibility?

Each pattern description covers the problem it solves, the solution structure, the tradeoffs, and the consequences. The point is not to memorize implementations - it is to recognize the problem shapes and have vocabulary for discussing solutions.

The vocabulary matters. When a senior engineer says “let’s use a facade here” or “this is a strategy pattern,” the entire team immediately understands the proposed structure. Without the vocabulary, they would need ten minutes of whiteboard explanation. Patterns compress communication.

Creational Patterns

Singleton ensures only one instance of a class exists throughout the application’s lifetime:

class Config:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._settings = {}
        return cls._instance

    def set(self, key: str, value) -> None:
        self._settings[key] = value

    def get(self, key: str, default=None):
        return self._settings.get(key, default)

# No matter how many times you call Config(), you get the same object
config1 = Config()
config2 = Config()
config1.set("debug", True)
print(config2.get("debug"))   # True - same object
print(config1 is config2)      # True

Singleton is appropriate for resources that genuinely should be unique: configuration stores, logging systems, connection pools. It is inappropriate for everything else - it is global state, with all the testing and coupling problems that implies. Every Singleton is a hidden dependency. Tests that rely on global state interfere with each other unless carefully reset between runs.

In Python specifically, module-level objects are already singletons. A module is loaded once; every import of the same module returns the same object. import logging; logging.getLogger("myapp") gives you the same logger everywhere. You rarely need the __new__ trick.

Factory decouples object creation from object use:

class Animal:
    def speak(self) -> str:
        raise NotImplementedError

class Dog(Animal):
    def speak(self) -> str:
        return "Woof!"

class Cat(Animal):
    def speak(self) -> str:
        return "Meow."

class Bird(Animal):
    def speak(self) -> str:
        return "Tweet!"

def animal_factory(kind: str) -> Animal:
    registry = {"dog": Dog, "cat": Cat, "bird": Bird}
    cls = registry.get(kind.lower())
    if cls is None:
        raise ValueError(f"Unknown animal: {kind!r}")
    return cls()

animal = animal_factory("dog")
print(animal.speak())   # "Woof!"

The caller asks for an animal by name and receives an object. The exact class is an implementation detail. You can add a new animal type by adding it to the registry - no changes to calling code.

Builder constructs complex objects step by step, and is most useful when an object has many optional parameters:

class QueryBuilder:
    def __init__(self):
        self._table = None
        self._fields = ["*"]
        self._conditions = []
        self._order_by = None
        self._limit = None

    def from_table(self, table: str) -> "QueryBuilder":
        self._table = table
        return self   # return self enables method chaining

    def select(self, *fields: str) -> "QueryBuilder":
        self._fields = list(fields)
        return self

    def where(self, condition: str) -> "QueryBuilder":
        self._conditions.append(condition)
        return self

    def order_by(self, field: str) -> "QueryBuilder":
        self._order_by = field
        return self

    def limit(self, n: int) -> "QueryBuilder":
        self._limit = n
        return self

    def build(self) -> str:
        query = f"SELECT {', '.join(self._fields)} FROM {self._table}"
        if self._conditions:
            query += " WHERE " + " AND ".join(self._conditions)
        if self._order_by:
            query += f" ORDER BY {self._order_by}"
        if self._limit:
            query += f" LIMIT {self._limit}"
        return query

query = (QueryBuilder()
         .from_table("users")
         .select("name", "email")
         .where("active = true")
         .where("age > 18")
         .order_by("created_at")
         .limit(50)
         .build())

Django’s QuerySet and SQLAlchemy’s Query objects are Builder implementations. The method chaining is the signature.

Structural Patterns

Adapter wraps an incompatible interface to make it fit what you need:

class LegacyPaymentGateway:
    def process_transaction(self, amount_in_cents: int, card_number: str) -> bool:
        print(f"Processing {amount_in_cents} cents for card {card_number[-4:]}")
        return True

class ModernPaymentAdapter:
    """Adapts LegacyPaymentGateway to the new interface."""
    def __init__(self, legacy: LegacyPaymentGateway):
        self._legacy = legacy

    def pay(self, amount_dollars: float, card: str) -> bool:
        amount_cents = int(amount_dollars * 100)
        return self._legacy.process_transaction(amount_cents, card)

legacy = LegacyPaymentGateway()
adapter = ModernPaymentAdapter(legacy)
adapter.pay(29.99, "4111111111111234")   # modern interface, legacy implementation

Adapters appear everywhere third-party libraries and legacy systems must coexist. Python’s own io.TextIOWrapper wraps a binary stream to provide a text interface - that is an Adapter.

Decorator adds behavior to an object without subclassing. We covered Python’s function decorators in depth earlier - but the object-level version works the same way:

import time
from functools import wraps

def retry(max_attempts: int = 3, delay: float = 1.0):
    def decorator(func):
        @wraps(func)   # preserves __name__, __doc__, etc.
        def wrapper(*args, **kwargs):
            last_error = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_error = e
                    print(f"Attempt {attempt + 1} failed: {e}")
                    if attempt < max_attempts - 1:
                        time.sleep(delay)
            raise last_error
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def fetch_user(user_id: int) -> dict:
    # might fail with network errors
    ...

@wraps(func) preserves the original function’s metadata (name, docstring, signature). Always use it in decorators.

Facade provides a simple interface over a complex subsystem:

class EmailFacade:
    """A simple interface hiding SMTP configuration, template rendering,
    retry logic, and bounce handling."""

    def __init__(self, smtp_host: str, sender: str):
        self._smtp_host = smtp_host
        self._sender = sender

    def send_welcome(self, recipient: str, username: str) -> None:
        subject = "Welcome to the platform!"
        body = f"Hi {username},\n\nThanks for joining.\n\nBest,\nThe Team"
        self._send(recipient, subject, body)

    def send_password_reset(self, recipient: str, token: str) -> None:
        subject = "Password reset request"
        body = f"Click this link: https://example.com/reset?token={token}"
        self._send(recipient, subject, body)

    def _send(self, to: str, subject: str, body: str) -> None:
        # Internal: connects to SMTP, handles auth, retries, logging
        print(f"Sending '{subject}' to {to}")

The caller never sees SMTP, authentication tokens, connection pooling, or retry logic. They call email.send_welcome(user.email, user.name) and it works.

Behavioral Patterns

Observer (publish/subscribe) lets objects subscribe to events and be notified when they happen:

from typing import Callable, Any

class EventBus:
    def __init__(self):
        self._listeners: dict[str, list[Callable]] = {}

    def subscribe(self, event: str, callback: Callable[[Any], None]) -> None:
        self._listeners.setdefault(event, []).append(callback)

    def unsubscribe(self, event: str, callback: Callable) -> None:
        if event in self._listeners:
            self._listeners[event].remove(callback)

    def emit(self, event: str, data: Any = None) -> None:
        for callback in self._listeners.get(event, []):
            callback(data)

bus = EventBus()
bus.subscribe("user.signup", lambda d: print(f"Send welcome email to {d['email']}"))
bus.subscribe("user.signup", lambda d: print(f"Create audit log for {d['user_id']}"))

bus.emit("user.signup", {"user_id": 42, "email": "alice@example.com"})
# Send welcome email to alice@example.com
# Create audit log for 42

The emitter (emit) does not know who is listening. Listeners can be added and removed independently. New behavior (e.g., sending a Slack notification on signup) is added by subscribing, not by modifying the signup code. This is the open/closed principle: open for extension, closed for modification.

Observer powers virtually every event-driven system: UI frameworks (button clicks), message brokers (Kafka, RabbitMQ), WebSockets, Node.js’s EventEmitter, and game engine event systems.

Strategy encapsulates an algorithm behind an interface so it can be swapped at runtime:

from typing import Protocol

class SortStrategy(Protocol):
    def sort(self, data: list) -> list: ...

class BubbleSort:
    def sort(self, data: list) -> list:
        data = list(data)
        n = len(data)
        for i in range(n):
            for j in range(n - i - 1):
                if data[j] > data[j + 1]:
                    data[j], data[j + 1] = data[j + 1], data[j]
        return data

class Sorter:
    def __init__(self, strategy: SortStrategy):
        self._strategy = strategy

    def set_strategy(self, strategy: SortStrategy) -> None:
        self._strategy = strategy

    def sort(self, data: list) -> list:
        return self._strategy.sort(data)

sorter = Sorter(BubbleSort())
print(sorter.sort([3, 1, 4, 1, 5]))   # [1, 1, 3, 4, 5]

# Swap to the built-in sort at runtime
sorter.set_strategy(type("BuiltIn", (), {"sort": lambda self, d: sorted(d)})())

In Python, Strategy is often unnecessary - you can pass a function directly (sorted(data, key=func) is a Strategy). In languages without first-class functions, Strategy was the workaround. More on this below.

Command encapsulates an action as an object, enabling undo, logging, and queuing:

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self) -> None: ...

    @abstractmethod
    def undo(self) -> None: ...

class DepositCommand(Command):
    def __init__(self, account, amount: float):
        self.account = account
        self.amount = amount

    def execute(self) -> None:
        self.account.deposit(self.amount)

    def undo(self) -> None:
        self.account.withdraw(self.amount)

class CommandHistory:
    def __init__(self):
        self._history: list[Command] = []

    def execute(self, command: Command) -> None:
        command.execute()
        self._history.append(command)

    def undo_last(self) -> None:
        if self._history:
            self._history.pop().undo()

Command is the pattern behind undo/redo systems, task queues, and transaction logs. When you record what happened rather than just doing it, you get auditability and reversibility for free.

The Criticism: Patterns as Workarounds

Here is the argument that the GoF book does not make, but that is increasingly hard to dismiss.

In 1999, Paul Graham wrote that many design patterns are simply features that better languages have built in. Peter Norvig’s 1996 analysis showed that 16 of the 23 GoF patterns are either “invisible or simpler” in Lisp or Dylan, both of which have first-class functions.

The examples from this post illustrate it:

  • Strategy in Java requires a Strategy interface, a ConcreteStrategyA class, a ConcreteStrategyB class, and injection code. In Python, you pass a function.
  • Factory in Java often requires abstract factories with elaborate class hierarchies. In Python, you put constructors in a dictionary and look them up.
  • Command in Java requires a Command interface and a concrete class per action. In Python, a lambda or a partial function often suffices.
  • Iterator is a GoF pattern. In Python, it is built into the language - anything with __iter__ and __next__ is an iterator.
  • Template Method (a parent class defines the skeleton of an algorithm and defers some steps to subclasses) is often replaced in Python by passing a callback function.

This is not a criticism of the GoF book - it is a remarkable piece of work and the vocabulary it gave us is genuinely valuable. The criticism is of the cargo cult that emerged around it: junior engineers treating design patterns as goals rather than tools, manufacturing elaborate class hierarchies to implement patterns that solve problems they do not have.

The Singleton is the most abused. It is global state with a constructor. Every time you reach for it, ask: can I just use a module-level variable? Can I pass this dependency explicitly? Usually yes.

Where Patterns Live Today

The Gang of Four patterns have not become irrelevant - they have been absorbed.

Modern web frameworks implement patterns so thoroughly that you use them without naming them:

  • Django models are the Active Record pattern (data object + database persistence).
  • Django views and middleware use the Chain of Responsibility pattern.
  • Flask’s @app.route is the Front Controller and Command patterns.
  • React’s component tree uses Composite (components containing components) and Observer (state changes propagating down).
  • Redux is an explicit implementation of Command and Observer.
  • SQLAlchemy’s Unit of Work tracks all changes during a session and commits them atomically.

You are already using these patterns. Understanding their names helps you understand why the frameworks are designed the way they are, and what tradeoffs were made.

The patterns that have grown more relevant since 1994:

Dependency Injection - pass dependencies into objects rather than having them create their own. This is the antidote to Singleton and makes testing tractable. FastAPI’s dependency injection system is a first-class feature.

Repository - abstract the data access layer behind an interface, so business logic does not know whether it is talking to PostgreSQL, MongoDB, or a test fixture.

Event Sourcing - store the history of commands (Command pattern), not just the current state. The current state is derived by replaying the command log. Kafka and Confluent have built businesses on this idea.

The Future: Less Pattern, More Principle

The patterns that will matter most going forward are less about class hierarchies and more about system design:

  • Idempotency: designing operations so they can be retried safely.
  • Backpressure: controlling the rate of data flow through a pipeline.
  • Circuit breakers: stopping calls to failing services rather than letting failures cascade.
  • CQRS (Command Query Responsibility Segregation): separate the read and write models.

These are not GoF patterns. They are architectural patterns for distributed systems, and they will be more relevant to most engineers than Visitor or Memento.

The principle beneath all of it has not changed since Alexander wrote about architecture: recurring problems deserve named solutions. When you recognize the shape of a problem, you can reach for a known solution rather than inventing one from scratch. That is not a Java-era idea. It is a fundamental property of engineering knowledge.


Pattern Category What it solves Python note
Singleton Creational One shared instance Module objects are already singletons
Factory Creational Decouple creation from use Often a dict lookup in Python
Builder Creational Complex object construction Method chaining
Adapter Structural Incompatible interfaces Wrap-and-delegate
Decorator Structural Add behavior without subclassing Python @decorator syntax
Facade Structural Simple interface over complexity Hide subsystem details
Observer Behavioral Event-driven notification Powers every UI and message broker
Strategy Behavioral Swap algorithms at runtime Often just a function argument in Python
Command Behavioral Encapsulate actions as objects Enables undo, queues, audit logs

Read Next: