Most Design Patterns Are Solutions to Problems You Don't Have
The Gang of Four book describes 23 patterns. In 15 years of writing production code, I regularly use maybe 6 of them. The rest solve problems specific to C++ circa 1994 — rigid class hierarchies, no first-class functions, no generics. Modern languages have absorbed many of these patterns into their standard features.
Iterator? It's a for loop. Observer? It's an event emitter. Command? It's a function. Template Method? It's a function that takes a callback.
But a handful of patterns remain genuinely useful because they address structural problems that haven't gone away. Here are the ones that show up in real codebases.
Strategy: Swappable Behavior Without Conditionals
The problem: you need different algorithms or behaviors based on context, and you're tired of giant if/else chains.
# Python: functions are already strategies
from typing import Callable
PricingStrategy = Callable[[float, int], float]
def standard_pricing(base_price: float, quantity: int) -> float:
return base_price * quantity
def bulk_pricing(base_price: float, quantity: int) -> float:
if quantity >= 100:
return base_price * quantity * 0.8
return base_price * quantity * 0.9
def seasonal_pricing(base_price: float, quantity: int) -> float:
return base_price * quantity * 0.7
def calculate_order(
items: list[tuple[float, int]],
pricing: PricingStrategy = standard_pricing,
) -> float:
return sum(pricing(price, qty) for price, qty in items)
# Usage
total = calculate_order(items, bulk_pricing)
In languages with first-class functions, Strategy is just "pass a function." No interfaces, no abstract base classes, no PricingStrategyFactory. The pattern is valuable — the traditional OOP ceremony around it isn't.
TypeScript version:
type PricingStrategy = (basePrice: number, quantity: number) => number;
const bulkPricing: PricingStrategy = (price, qty) =>
qty >= 100 ? price * qty * 0.8 : price * qty * 0.9;
function calculateOrder(
items: [number, number][],
pricing: PricingStrategy = (p, q) => p * q,
): number {
return items.reduce((sum, [price, qty]) => sum + pricing(price, qty), 0);
}
Observer: Decoupling Event Producers From Consumers
The problem: when something happens in module A, modules B, C, and D need to know about it, but A shouldn't depend on B, C, or D.
In Node.js, EventEmitter is the Observer pattern built in:
const EventEmitter = require('events');
class OrderService extends EventEmitter {
async createOrder(data) {
const order = await this.db.insert(data);
// OrderService doesn't know who's listening
this.emit('order:created', order);
return order;
}
}
// Separate modules subscribe independently
orderService.on('order:created', (order) => emailService.sendConfirmation(order));
orderService.on('order:created', (order) => inventoryService.reserve(order));
orderService.on('order:created', (order) => analyticsService.track('purchase', order));
The value here is real: OrderService can be tested without mocking the email service, inventory service, or analytics. New consumers can be added without modifying OrderService.
The trap: overusing events makes control flow invisible. When a bug occurs during order creation, you have to trace through event handlers to find it. I'd argue observer is best for side effects (notifications, logging, analytics) — not for core business logic where explicit function calls are clearer.
Builder: Constructing Complex Objects Step by Step
The problem: creating an object that has many optional parameters. Constructors with 15 arguments are unreadable. Named parameters help (Python, Kotlin) but don't enforce valid combinations.
class QueryBuilder:
def __init__(self, table: str):
self._table = table
self._conditions: list[str] = []
self._params: list = []
self._order: str | None = None
self._limit: int | None = None
self._offset: int | None = None
self._columns: list[str] = ["*"]
def select(self, *columns: str) -> "QueryBuilder":
self._columns = list(columns)
return self
def where(self, condition: str, *params) -> "QueryBuilder":
self._conditions.append(condition)
self._params.extend(params)
return self
def order_by(self, column: str, direction: str = "ASC") -> "QueryBuilder":
self._order = f"{column} {direction}"
return self
def limit(self, n: int) -> "QueryBuilder":
self._limit = n
return self
def offset(self, n: int) -> "QueryBuilder":
self._offset = n
return self
def build(self) -> tuple[str, list]:
sql = f"SELECT {', '.join(self._columns)} FROM {self._table}"
if self._conditions:
sql += " WHERE " + " AND ".join(self._conditions)
if self._order:
sql += f" ORDER BY {self._order}"
if self._limit is not None:
sql += f" LIMIT {self._limit}"
if self._offset is not None:
sql += f" OFFSET {self._offset}"
return sql, self._params
# Usage
query, params = (
QueryBuilder("users")
.select("id", "name", "email")
.where("status = %s", "active")
.where("created_at > %s", "2026-01-01")
.order_by("created_at", "DESC")
.limit(20)
.build()
)
Builder shines when the construction steps matter — when the object is immutable after creation, when certain combinations of parameters are invalid, or when you want IDE autocomplete to guide the user through the configuration.
Decorator: Adding Behavior Without Modifying Code
Python has decorators baked into the syntax. TypeScript has them too (stage 3 proposal, widely used with experimental flag). The pattern is universally useful for cross-cutting concerns:
import functools
import time
import logging
logger = logging.getLogger(__name__)
def retry(max_attempts: int = 3, delay: float = 1.0):
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
logger.warning(f"{func.__name__} failed (attempt {attempt}): {e}")
time.sleep(delay * attempt)
return wrapper
return decorator
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
logger.info(f"{func.__name__} took {elapsed:.3f}s")
return result
return wrapper
@retry(max_attempts=3)
@timed
def fetch_external_data(url: str) -> dict:
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
Decorator composes cleanly: @retry @timed applies both behaviors without either function knowing about the other. This is the open/closed principle in practice — extending behavior without modifying existing code.
When to Reach for a Pattern
Don't start with a pattern. Start with the simplest code that solves the problem. When you find yourself writing the third if strategy == "bulk" branch, that's when Strategy earns its place. When the constructor has 12 optional parameters, that's when Builder makes sense.
Patterns are refactoring targets, not starting points. The code tells you when it needs more structure — listen to it rather than imposing structure preemptively.