thecodex.expert · The Codex Family of Knowledge
Software Engineering

Design Patterns

Reusable solutions to problems that recur in software design. Not templates to copy — vocabulary for describing how things should be structured.

GoF — 23 patterns Creational · Structural · Behavioural Last verified:
Canonical Definition

Design patterns are reusable solutions to commonly occurring problems in software design — documented templates for solving a class of problems, not finished code. The Gang of Four (GoF) book by Gamma, Helm, Johnson, and Vlissides (1994) catalogued 23 patterns in three categories: Creational (how objects are created), Structural (how objects are composed), and Behavioural (how objects communicate).

How to use this reference

Patterns are solutions to problems. Learn to recognise the problem first, then the pattern naturally follows. Don't force patterns onto code that doesn't need them — over-engineering is as harmful as under-engineering. If someone has to explain why a pattern is here, the pattern is probably wrong.

Creational Patterns

Creational patterns deal with object creation — abstracting the instantiation process to make a system independent of how its objects are created, composed, and represented.

Singleton — one instance, globally accessible

Problem: You need exactly one instance of a class — a configuration manager, a connection pool, a logging service. Creating multiple instances would be incorrect or wasteful.

Solution: Restrict instantiation to one object. Provide a global access point to that instance.

Pythonsingleton.py
class DatabasePool:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._connections = []
        return cls._instance

    def get_connection(self):
        return self._connections[0] if self._connections else None

# Both variables point to the same object
a = DatabasePool()
b = DatabasePool()
assert a is b  # True

When to use: Logging, configuration, connection pools, caches. When to avoid: Singletons make testing hard (global state persists between tests) and hide dependencies. Prefer dependency injection — pass the shared object explicitly rather than accessing it globally.

Factory Method — let subclasses decide what to create

Problem: A class needs to create objects but shouldn't decide exactly which class to instantiate — subclasses or callers should decide.

Pythonfactory.py
from abc import ABC, abstractmethod

class Notification(ABC):
    @abstractmethod
    def send(self, message: str) -> None: ...

class EmailNotification(Notification):
    def send(self, message: str) -> None:
        print(f"Email: {message}")

class SMSNotification(Notification):
    def send(self, message: str) -> None:
        print(f"SMS: {message}")

class PushNotification(Notification):
    def send(self, message: str) -> None:
        print(f"Push: {message}")

# Factory function — centralises creation logic
def create_notification(channel: str) -> Notification:
    match channel:
        case "email": return EmailNotification()
        case "sms":   return SMSNotification()
        case "push":  return PushNotification()
        case _:       raise ValueError(f"Unknown channel: {channel}")

# Caller doesn't know or care which class it gets
notif = create_notification("email")
notif.send("Your order shipped!")

Builder — construct complex objects step by step

Problem: Creating an object requires many parameters, some optional, and the right combination varies by use case. A constructor with 12 parameters is unreadable.

Pythonbuilder.py
from dataclasses import dataclass, field
from typing import Self

@dataclass
class HttpRequest:
    url: str
    method: str = "GET"
    headers: dict = field(default_factory=dict)
    body: str | None = None
    timeout_ms: int = 5000

class HttpRequestBuilder:
    def __init__(self, url: str):
        self._req = HttpRequest(url=url)

    def method(self, m: str) -> Self:
        self._req.method = m; return self

    def header(self, key: str, value: str) -> Self:
        self._req.headers[key] = value; return self

    def body(self, body: str) -> Self:
        self._req.body = body; return self

    def timeout(self, ms: int) -> Self:
        self._req.timeout_ms = ms; return self

    def build(self) -> HttpRequest:
        return self._req

req = (HttpRequestBuilder("https://api.example.com/users")
       .method("POST")
       .header("Content-Type", "application/json")
       .header("Authorization", "Bearer token")
       .body('{"name":"Alice"}')
       .timeout(10_000)
       .build())

Structural Patterns

Structural patterns deal with how classes and objects are composed to form larger structures.

Adapter — make incompatible interfaces work together

Problem: You have existing code that expects interface A, and a library that provides interface B. You can't change either. You need to make them work together.

TypeScriptadapter.ts
// Your system expects this interface
interface Logger {
    log(level: "info" | "warn" | "error", message: string): void;
}

// Third-party library has this different interface
class LegacyLoggingLib {
    info(msg: string)  { console.log(`[INFO] ${msg}`); }
    warning(msg: string) { console.warn(`[WARN] ${msg}`); }
    critical(msg: string) { console.error(`[CRIT] ${msg}`); }
}

// Adapter: wraps the legacy lib, presents the expected interface
class LegacyLoggerAdapter implements Logger {
    constructor(private legacy: LegacyLoggingLib) {}

    log(level: "info" | "warn" | "error", message: string): void {
        switch (level) {
            case "info":  this.legacy.info(message); break;
            case "warn":  this.legacy.warning(message); break;
            case "error": this.legacy.critical(message); break;
        }
    }
}

// Existing code works unchanged
function processOrder(logger: Logger) {
    logger.log("info", "Order received");
}

const adapter = new LegacyLoggerAdapter(new LegacyLoggingLib());
processOrder(adapter);  // works!

Decorator — add behaviour without modifying the original

Problem: You want to add functionality (logging, caching, authentication, retry logic) to an existing class without changing it or creating a class hierarchy explosion.

Pythondecorator_pattern.py
import time, functools
from typing import Callable

# The "decorator pattern" as a Python decorator (same idea, different syntax)
def timed(func: Callable) -> Callable:
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.3f}s")
        return result
    return wrapper

def retry(max_attempts: int = 3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts: raise
                    print(f"Attempt {attempt} failed: {e}")
        return wrapper
    return decorator

# Stack decorators — each adds behaviour without changing the original
@timed
@retry(max_attempts=3)
def fetch_data(url: str) -> str:
    return f"data from {url}"

Facade — simplify a complex subsystem

Problem: A subsystem has many classes with complex interactions. Clients shouldn't need to understand all of it.

TypeScriptfacade.ts
// Complex subsystem with many moving parts
class VideoDecoder { decode(path: string) { return `raw:${path}`; } }
class AudioDecoder { decode(path: string) { return `audio:${path}`; } }
class VideoEncoder { encode(data: string, format: string) { return `${format}:${data}`; } }
class FileSystem   { write(path: string, data: string) { console.log(`Wrote ${path}`); } }

// Facade: one simple interface over all of them
class VideoConverter {
    private video = new VideoDecoder();
    private audio = new AudioDecoder();
    private encoder = new VideoEncoder();
    private fs = new FileSystem();

    convert(inputPath: string, outputPath: string, format: string): void {
        const videoData = this.video.decode(inputPath);
        const audioData = this.audio.decode(inputPath);
        const encoded = this.encoder.encode(videoData + audioData, format);
        this.fs.write(outputPath, encoded);
    }
}

// Client only needs to know about the facade
const converter = new VideoConverter();
converter.convert("input.mov", "output.mp4", "h264");

Behavioural Patterns

Behavioural patterns deal with algorithms and the assignment of responsibilities between objects.

Observer — event-driven notification

Problem: When one object changes state, an unknown number of other objects need to know about it. You don't want tight coupling between them.

TypeScriptobserver.ts
type EventMap = Record<string, unknown>;

class EventEmitter<Events extends EventMap> {
    private handlers = new Map<keyof Events, Set<Function>>();

    on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void): () => void {
        if (!this.handlers.has(event)) this.handlers.set(event, new Set());
        this.handlers.get(event)!.add(handler);
        return () => this.handlers.get(event)?.delete(handler); // unsubscribe
    }

    emit<K extends keyof Events>(event: K, data: Events[K]): void {
        this.handlers.get(event)?.forEach(h => h(data));
    }
}

// Usage
type OrderEvents = {
    placed:   { orderId: string; total: number };
    shipped:  { orderId: string; trackingId: string };
    delivered: { orderId: string };
};

const orders = new EventEmitter<OrderEvents>();

const unsub = orders.on("placed", ({ orderId, total }) => {
    console.log(`Send confirmation email for order ${orderId} (${total})`);
});

orders.on("placed", ({ orderId }) => {
    console.log(`Notify warehouse for order ${orderId}`);
});

orders.emit("placed", { orderId: "ord-123", total: 59.99 });
unsub();  // stop receiving "placed" events

Strategy — swap algorithms at runtime

Problem: You have a class that needs to perform some operation, but the algorithm should vary. Hard-coding the algorithm means you need different classes for each variation.

Pythonstrategy.py
from abc import ABC, abstractmethod
from typing import Protocol

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

class QuickSort:
    def sort(self, data: list) -> list:
        if len(data) <= 1: return data
        pivot = data[len(data)//2]
        left  = [x for x in data if x < pivot]
        mid   = [x for x in data if x == pivot]
        right = [x for x in data if x > pivot]
        return self.sort(left) + mid + self.sort(right)

class MergeSort:
    def sort(self, data: list) -> list:
        return sorted(data)  # simplified

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

    def set_strategy(self, strategy: SortStrategy) -> None:
        self._strategy = strategy  # swap at runtime

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

sorter = Sorter(QuickSort())
print(sorter.sort([5, 2, 8, 1, 9]))  # [1, 2, 5, 8, 9]

sorter.set_strategy(MergeSort())  # swap
print(sorter.sort([5, 2, 8, 1, 9]))  # [1, 2, 5, 8, 9]

Command — encapsulate actions as objects

Problem: You want to parameterise objects with actions, queue operations, support undo/redo, or log operations.

TypeScriptcommand.ts
interface Command {
    execute(): void;
    undo(): void;
}

class TextEditor {
    private text = "";
    private history: Command[] = [];

    executeCommand(cmd: Command): void {
        cmd.execute();
        this.history.push(cmd);
    }

    undoLast(): void {
        this.history.pop()?.undo();
    }

    getText(): string { return this.text; }
    setText(t: string): void { this.text = t; }
}

class InsertCommand implements Command {
    private previous = "";
    constructor(
        private editor: TextEditor,
        private text: string
    ) {}

    execute(): void {
        this.previous = this.editor.getText();
        this.editor.setText(this.previous + this.text);
    }

    undo(): void { this.editor.setText(this.previous); }
}

const editor = new TextEditor();
editor.executeCommand(new InsertCommand(editor, "Hello"));
editor.executeCommand(new InsertCommand(editor, ", World!"));
console.log(editor.getText());  // "Hello, World!"
editor.undoLast();
console.log(editor.getText());  // "Hello"

Anti-patterns to avoid

Common pitfalls
God class. One class that knows and does too much. The class ends up with 50 methods and hundreds of lines. Split by responsibility.
Shotgun surgery. Making one logical change requires editing many different classes. Usually a sign that related code is scattered — consolidate related responsibilities.
Premature pattern application. "We should use Strategy here in case we need to swap the algorithm later" — but you never do. Patterns add complexity; only add them when the problem they solve actually exists.
Singleton abuse. Using Singleton because you want global access, not because you need exactly one instance. Global state makes testing painful and dependencies invisible. Prefer dependency injection.
How this connects
Background

Sources

1
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — The original GoF reference.
2
Fowler, M. (1999). Refactoring: Improving the Design of Existing Code. Addison-Wesley. — Anti-patterns and code smells.
3
Refactoring.guru. Design Patterns. refactoring.guru/design-patterns. — Visual explanations with code examples in multiple languages.
Source confidence: High Last verified: Primary source: GoF Design Patterns (1994)