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).
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.
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 # TrueWhen 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.
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.
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.
// 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.
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.
// 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.
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" eventsStrategy — 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.
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.
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"