Software Design Patterns: A Comprehensive Guide with TypeScript & Python Examples
Master the 23 Gang of Four design patterns — Creational (Factory, Builder, Singleton), Structural (Adapter, Decorator, Proxy, Facade), and Behavioral (Observer, Strategy, Command, State) — with production-ready TypeScript and Python code examples.
- Factory: Delegate object creation to factory methods, decoupling clients from concrete classes
- Builder: Construct complex objects step by step with fluent chaining and optional params
- Singleton: Ensure a single global instance (use sparingly — prefer DI)
- Adapter / Facade: Unify incompatible interfaces / provide a simple entry to complex subsystems
- Decorator / Proxy: Add behavior dynamically / control object access (cache, logging, auth)
- Observer: One-to-many notification driving event systems and reactive programming
- Strategy / Command / State: Swappable algorithms / encapsulate requests / state-driven behavior
- Favor composition over inheritance — most patterns embody this principle
- Program to interfaces, not implementations — reduce coupling
- Open/Closed Principle (OCP): Open for extension, closed for modification
- Do not use patterns for their own sake — introduce them only when genuinely needed
- TypeScript interfaces and Python Protocols are keystones for type-safe patterns
- Modern frameworks embed many patterns (React Context=Observer, Express middleware=Chain of Responsibility)
Design Patterns Overview
Design patterns fall into three categories. The table below lists the 11 most commonly used patterns covered in this guide along with their core intent.
| Category | Pattern | Core Intent |
|---|---|---|
| Creational | Factory | Delegate object instantiation to subclasses or factory methods |
| Creational | Builder | Construct complex objects step by step |
| Creational | Singleton | Ensure a class has only one instance |
| Structural | Adapter | Convert incompatible interfaces into compatible ones |
| Structural | Decorator | Dynamically attach new behavior to objects |
| Structural | Proxy | Provide a surrogate to control access to an object |
| Structural | Facade | Provide a simplified interface to a complex subsystem |
| Behavioral | Observer | Define one-to-many dependency with automatic change notification |
| Behavioral | Strategy | Define a family of interchangeable algorithms |
| Behavioral | Command | Encapsulate a request as an object, enabling undo and queuing |
| Behavioral | State | Alter object behavior when its internal state changes |
1. Creational Patterns
1.1 Factory Method Pattern
The Factory Method defines an interface for creating an object but lets subclasses decide which class to instantiate. It defers the new operation to subclasses, keeping code open for extension and closed for modification.
TypeScript
// Factory Method — TypeScript
interface Notification {
send(message: string): void;
}
class EmailNotification implements Notification {
send(message: string): void {
console.log("Email: " + message);
}
}
class SMSNotification implements Notification {
send(message: string): void {
console.log("SMS: " + message);
}
}
class PushNotification implements Notification {
send(message: string): void {
console.log("Push: " + message);
}
}
type Channel = "email" | "sms" | "push";
function createNotification(channel: Channel): Notification {
const map: Record<Channel, () => Notification> = {
email: () => new EmailNotification(),
sms: () => new SMSNotification(),
push: () => new PushNotification(),
};
return map[channel]();
}
// Usage
const notifier = createNotification("email");
notifier.send("Welcome aboard!"); // Email: Welcome aboard!Python
# Factory Method — Python
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}")
def create_notification(channel: str) -> Notification:
factories = {
"email": EmailNotification,
"sms": SMSNotification,
"push": PushNotification,
}
if channel not in factories:
raise ValueError(f"Unknown channel: {channel}")
return factories[channel]()
# Usage
notifier = create_notification("sms")
notifier.send("Your code is 1234") # SMS: Your code is 12341.2 Builder Pattern
The Builder pattern separates the construction of a complex object from its representation. When an object has many optional parameters or requires a specific build order, Builder is more elegant than constructor overloading.
TypeScript
// Builder — TypeScript
interface QueryConfig {
table: string;
fields: string[];
conditions: string[];
orderBy?: string;
limit?: number;
}
class QueryBuilder {
private config: QueryConfig;
constructor(table: string) {
this.config = { table, fields: ["*"], conditions: [] };
}
select(...fields: string[]): this {
this.config.fields = fields;
return this;
}
where(condition: string): this {
this.config.conditions.push(condition);
return this;
}
orderBy(field: string): this {
this.config.orderBy = field;
return this;
}
limit(n: number): this {
this.config.limit = n;
return this;
}
build(): string {
let sql = "SELECT " + this.config.fields.join(", ");
sql += " FROM " + this.config.table;
if (this.config.conditions.length > 0) {
sql += " WHERE " + this.config.conditions.join(" AND ");
}
if (this.config.orderBy) {
sql += " ORDER BY " + this.config.orderBy;
}
if (this.config.limit !== undefined) {
sql += " LIMIT " + this.config.limit;
}
return sql;
}
}
// Usage — fluent chaining
const query = new QueryBuilder("users")
.select("id", "name", "email")
.where("active = true")
.where("age > 18")
.orderBy("name")
.limit(50)
.build();
console.log(query);
// SELECT id, name, email FROM users WHERE active = true AND age > 18 ORDER BY name LIMIT 50Python
# Builder — Python
from dataclasses import dataclass, field
@dataclass
class QueryConfig:
table: str
fields: list[str] = field(default_factory=lambda: ["*"])
conditions: list[str] = field(default_factory=list)
order_by: str | None = None
limit: int | None = None
class QueryBuilder:
def __init__(self, table: str):
self._config = QueryConfig(table=table)
def select(self, *fields: str) -> "QueryBuilder":
self._config.fields = list(fields)
return self
def where(self, condition: str) -> "QueryBuilder":
self._config.conditions.append(condition)
return self
def order_by(self, col: str) -> "QueryBuilder":
self._config.order_by = col
return self
def limit(self, n: int) -> "QueryBuilder":
self._config.limit = n
return self
def build(self) -> str:
sql = f"SELECT {', '.join(self._config.fields)} FROM {self._config.table}"
if self._config.conditions:
sql += f" WHERE {' AND '.join(self._config.conditions)}"
if self._config.order_by:
sql += f" ORDER BY {self._config.order_by}"
if self._config.limit is not None:
sql += f" LIMIT {self._config.limit}"
return sql
# Usage
query = (
QueryBuilder("users")
.select("id", "name", "email")
.where("active = true")
.order_by("name")
.limit(50)
.build()
)
print(query)1.3 Singleton Pattern
Singleton ensures a class has exactly one instance with a global access point. In modern development, dependency injection is often preferred, but Singleton remains useful for managing configuration, logging, and connection pools.
TypeScript
// Singleton — TypeScript
class Logger {
private static instance: Logger;
private logs: string[] = [];
private constructor() {} // prevent external instantiation
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(message: string): void {
const entry = "[" + new Date().toISOString() + "] " + message;
this.logs.push(entry);
console.log(entry);
}
getHistory(): string[] {
return [...this.logs];
}
}
// Usage — same instance everywhere
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // true
logger1.log("App started");
logger2.log("User logged in");
console.log(logger1.getHistory().length); // 2Python
# Singleton — Python (using __new__)
class Logger:
_instance = None
_logs: list[str] = []
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def log(self, message: str) -> None:
from datetime import datetime
entry = f"[{datetime.now().isoformat()}] {message}"
self._logs.append(entry)
print(entry)
def get_history(self) -> list[str]:
return list(self._logs)
# Usage
logger1 = Logger()
logger2 = Logger()
print(logger1 is logger2) # True
# Alternative: module-level singleton (Pythonic approach)
# config.py
class _Config:
def __init__(self):
self.debug = False
self.db_url = "sqlite:///app.db"
config = _Config() # module-level instance acts as singleton2. Structural Patterns
2.1 Adapter Pattern
The Adapter pattern converts the interface of a class into another interface clients expect. It allows classes with incompatible interfaces to work together, commonly used for integrating third-party libraries or legacy systems.
TypeScript
// Adapter — TypeScript
// Target interface our app expects
interface PaymentProcessor {
charge(amount: number, currency: string): Promise<{ id: string; status: string }>;
}
// Legacy third-party SDK with incompatible interface
class LegacyPaymentSDK {
makePayment(cents: number, curr: string, callback: (err: Error | null, txId: string) => void): void {
setTimeout(() => callback(null, "txn_" + Math.random().toString(36).slice(2)), 100);
}
}
// Adapter wraps the legacy SDK to match our interface
class LegacyPaymentAdapter implements PaymentProcessor {
constructor(private sdk: LegacyPaymentSDK) {}
charge(amount: number, currency: string): Promise<{ id: string; status: string }> {
return new Promise((resolve, reject) => {
const cents = Math.round(amount * 100);
this.sdk.makePayment(cents, currency, (err, txId) => {
if (err) reject(err);
else resolve({ id: txId, status: "success" });
});
});
}
}
// Usage — client only knows PaymentProcessor
async function checkout(processor: PaymentProcessor) {
const result = await processor.charge(29.99, "USD");
console.log("Payment " + result.id + ": " + result.status);
}
const adapter = new LegacyPaymentAdapter(new LegacyPaymentSDK());
checkout(adapter);Python
# Adapter — Python
from typing import Protocol
class PaymentProcessor(Protocol):
def charge(self, amount: float, currency: str) -> dict: ...
class LegacyPaymentSDK:
"""Third-party SDK with incompatible interface"""
def make_payment(self, cents: int, curr: str) -> str:
import uuid
return f"txn_{uuid.uuid4().hex[:8]}"
class LegacyPaymentAdapter:
"""Adapter wraps legacy SDK to match PaymentProcessor protocol"""
def __init__(self, sdk: LegacyPaymentSDK):
self._sdk = sdk
def charge(self, amount: float, currency: str) -> dict:
cents = round(amount * 100)
tx_id = self._sdk.make_payment(cents, currency)
return {"id": tx_id, "status": "success"}
# Usage
adapter = LegacyPaymentAdapter(LegacyPaymentSDK())
result = adapter.charge(29.99, "USD")
print(f"Payment {result['id']}: {result['status']}")2.2 Decorator Pattern
The Decorator pattern dynamically attaches additional responsibilities to an object without modifying the original class. It is more flexible than inheritance and allows combining behaviors freely. Python's @decorator syntax and TypeScript Stage 3 decorators are both influenced by this pattern.
TypeScript
// Decorator — TypeScript
interface HttpClient {
request(url: string, options?: RequestInit): Promise<Response>;
}
// Base implementation
class FetchClient implements HttpClient {
async request(url: string, options?: RequestInit): Promise<Response> {
return fetch(url, options);
}
}
// Decorator: adds logging
class LoggingDecorator implements HttpClient {
constructor(private client: HttpClient) {}
async request(url: string, options?: RequestInit): Promise<Response> {
console.log("[LOG] Request: " + url);
const start = Date.now();
const response = await this.client.request(url, options);
console.log("[LOG] " + response.status + " in " + (Date.now() - start) + "ms");
return response;
}
}
// Decorator: adds retry logic
class RetryDecorator implements HttpClient {
constructor(private client: HttpClient, private maxRetries = 3) {}
async request(url: string, options?: RequestInit): Promise<Response> {
let lastError: Error | null = null;
for (let i = 0; i <= this.maxRetries; i++) {
try {
return await this.client.request(url, options);
} catch (err) {
lastError = err as Error;
console.log("[RETRY] Attempt " + (i + 1) + " failed");
}
}
throw lastError;
}
}
// Usage: compose decorators
const client: HttpClient = new LoggingDecorator(
new RetryDecorator(
new FetchClient(), 3
)
);
// client.request("https://api.example.com/data");Python
# Decorator — Python (both class-based and @decorator syntax)
import functools
import time
from typing import Callable, Any
# Function decorator for timing
def timer(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
# Function decorator for retry
def retry(max_attempts: int = 3):
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt} failed: {e}")
if attempt == max_attempts:
raise
return wrapper
return decorator
# Usage — stack decorators
@timer
@retry(max_attempts=3)
def fetch_data(url: str) -> str:
import urllib.request
return urllib.request.urlopen(url).read().decode()
# fetch_data("https://api.example.com/data")2.3 Proxy Pattern
The Proxy pattern provides a surrogate or placeholder for another object to control access. Common use cases include lazy initialization (virtual proxy), access control (protection proxy), and caching (smart proxy).
TypeScript
// Proxy — TypeScript (Caching Proxy)
interface DataService {
fetchData(key: string): Promise<string>;
}
class ApiService implements DataService {
async fetchData(key: string): Promise<string> {
console.log("[API] Fetching " + key + " from server...");
// Simulate network request
return "data_for_" + key;
}
}
class CachingProxy implements DataService {
private cache = new Map<string, { value: string; expiry: number }>();
constructor(private service: DataService, private ttlMs = 60000) {}
async fetchData(key: string): Promise<string> {
const cached = this.cache.get(key);
if (cached && cached.expiry > Date.now()) {
console.log("[CACHE] Hit for " + key);
return cached.value;
}
console.log("[CACHE] Miss for " + key);
const value = await this.service.fetchData(key);
this.cache.set(key, { value, expiry: Date.now() + this.ttlMs });
return value;
}
}
// Usage
const service: DataService = new CachingProxy(new ApiService(), 5000);
// First call: cache miss, hits API
// Second call: cache hit, returns instantlyPython
# Proxy — Python (Caching Proxy)
import time
from typing import Protocol
class DataService(Protocol):
def fetch_data(self, key: str) -> str: ...
class ApiService:
def fetch_data(self, key: str) -> str:
print(f"[API] Fetching {key} from server...")
return f"data_for_{key}"
class CachingProxy:
def __init__(self, service: DataService, ttl: float = 60.0):
self._service = service
self._ttl = ttl
self._cache: dict[str, tuple[str, float]] = {}
def fetch_data(self, key: str) -> str:
if key in self._cache:
value, expiry = self._cache[key]
if time.time() < expiry:
print(f"[CACHE] Hit for {key}")
return value
print(f"[CACHE] Miss for {key}")
value = self._service.fetch_data(key)
self._cache[key] = (value, time.time() + self._ttl)
return value
# Usage
service = CachingProxy(ApiService(), ttl=5.0)
print(service.fetch_data("users")) # [API] Fetching...
print(service.fetch_data("users")) # [CACHE] Hit2.4 Facade Pattern
The Facade pattern provides a simplified, unified interface to a complex subsystem. It does not add new functionality but wraps multiple complex operations behind a high-level API, reducing usage complexity for clients.
TypeScript
// Facade — TypeScript
// Complex subsystems
class UserService {
createUser(email: string) { return { id: 1, email }; }
}
class EmailService {
sendWelcome(email: string) { console.log("Welcome email sent to " + email); }
}
class AnalyticsService {
trackSignup(userId: number) { console.log("Signup tracked for user " + userId); }
}
class BillingService {
createTrialSubscription(userId: number) {
console.log("14-day trial started for user " + userId);
}
}
// Facade — single method coordinates all subsystems
class RegistrationFacade {
private userService = new UserService();
private emailService = new EmailService();
private analytics = new AnalyticsService();
private billing = new BillingService();
async registerUser(email: string) {
const user = this.userService.createUser(email);
this.emailService.sendWelcome(email);
this.analytics.trackSignup(user.id);
this.billing.createTrialSubscription(user.id);
return user;
}
}
// Usage — client only interacts with the facade
const registration = new RegistrationFacade();
registration.registerUser("alice@example.com");3. Behavioral Patterns
3.1 Observer Pattern
The Observer pattern defines a one-to-many dependency between objects: when one object (Subject) changes state, all dependents (Observers) are notified and updated automatically. Event buses, pub/sub systems, and React state management all embody this pattern.
TypeScript
// Observer — TypeScript (Type-safe Event Emitter)
type EventMap = {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"cart:update": { items: number; total: number };
};
class EventBus<T extends Record<string, unknown>> {
private listeners = new Map<keyof T, Set<(data: any) => void>>();
on<K extends keyof T>(event: K, callback: (data: T[K]) => void): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
// Return unsubscribe function
return () => { this.listeners.get(event)?.delete(callback); };
}
emit<K extends keyof T>(event: K, data: T[K]): void {
this.listeners.get(event)?.forEach((cb) => cb(data));
}
}
// Usage
const bus = new EventBus<EventMap>();
const unsub = bus.on("user:login", (data) => {
console.log("User " + data.userId + " logged in at " + data.timestamp);
});
bus.on("cart:update", (data) => {
console.log("Cart: " + data.items + " items, $" + data.total);
});
bus.emit("user:login", { userId: "u_123", timestamp: Date.now() });
bus.emit("cart:update", { items: 3, total: 59.97 });
unsub(); // unsubscribe from user:loginPython
# Observer — Python
from typing import Callable, Any
from collections import defaultdict
class EventBus:
def __init__(self):
self._listeners: dict[str, list[Callable]] = defaultdict(list)
def on(self, event: str, callback: Callable) -> Callable:
self._listeners[event].append(callback)
def unsubscribe():
self._listeners[event].remove(callback)
return unsubscribe
def emit(self, event: str, **data: Any) -> None:
for callback in self._listeners.get(event, []):
callback(**data)
# Usage
bus = EventBus()
def on_login(user_id: str, timestamp: float):
print(f"User {user_id} logged in at {timestamp}")
unsub = bus.on("user:login", on_login)
bus.emit("user:login", user_id="u_123", timestamp=1709000000)
unsub() # unsubscribe3.2 Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Clients can select different strategies at runtime without modifying the code that uses them.
TypeScript
// Strategy — TypeScript
interface CompressionStrategy {
compress(data: string): string;
name: string;
}
class GzipStrategy implements CompressionStrategy {
name = "gzip";
compress(data: string): string {
// Simulate gzip compression
return "[gzip:" + data.length + "]" + data.slice(0, 10) + "...";
}
}
class BrotliStrategy implements CompressionStrategy {
name = "brotli";
compress(data: string): string {
// Simulate brotli compression (better ratio)
return "[br:" + Math.floor(data.length * 0.8) + "]" + data.slice(0, 8) + "...";
}
}
class NoCompression implements CompressionStrategy {
name = "none";
compress(data: string): string {
return data;
}
}
class FileCompressor {
constructor(private strategy: CompressionStrategy) {}
setStrategy(strategy: CompressionStrategy): void {
this.strategy = strategy;
}
compressFile(data: string): string {
console.log("Compressing with " + this.strategy.name);
return this.strategy.compress(data);
}
}
// Usage — switch strategies at runtime
const compressor = new FileCompressor(new GzipStrategy());
console.log(compressor.compressFile("Hello world repeated many times"));
compressor.setStrategy(new BrotliStrategy());
console.log(compressor.compressFile("Hello world repeated many times"));Python
# Strategy — Python (using Protocols and first-class functions)
from typing import Protocol, Callable
# Class-based approach
class CompressionStrategy(Protocol):
name: str
def compress(self, data: str) -> str: ...
class GzipStrategy:
name = "gzip"
def compress(self, data: str) -> str:
return f"[gzip:{len(data)}]{data[:10]}..."
class BrotliStrategy:
name = "brotli"
def compress(self, data: str) -> str:
return f"[br:{int(len(data)*0.8)}]{data[:8]}..."
class FileCompressor:
def __init__(self, strategy: CompressionStrategy):
self._strategy = strategy
def set_strategy(self, strategy: CompressionStrategy) -> None:
self._strategy = strategy
def compress_file(self, data: str) -> str:
print(f"Compressing with {self._strategy.name}")
return self._strategy.compress(data)
# Functional alternative — even simpler
CompressFn = Callable[[str], str]
def gzip_compress(data: str) -> str:
return f"[gzip:{len(data)}]{data[:10]}..."
def compress_file(data: str, strategy: CompressFn = gzip_compress) -> str:
return strategy(data)
print(compress_file("test data", gzip_compress))3.3 Command Pattern
The Command pattern encapsulates a request as a standalone object containing all information needed to perform the action. It supports parameterizing operations, queuing execution, logging, and undoable operations, forming the basis for undo/redo systems.
TypeScript
// Command — TypeScript (Text Editor with Undo/Redo)
interface Command {
execute(): void;
undo(): void;
description: string;
}
class TextEditor {
content = "";
cursorPos = 0;
}
class InsertTextCommand implements Command {
description: string;
private previousContent = "";
constructor(private editor: TextEditor, private text: string, private pos: number) {
this.description = "Insert \"" + text + "\" at pos " + pos;
}
execute(): void {
this.previousContent = this.editor.content;
const before = this.editor.content.slice(0, this.pos);
const after = this.editor.content.slice(this.pos);
this.editor.content = before + this.text + after;
this.editor.cursorPos = this.pos + this.text.length;
}
undo(): void {
this.editor.content = this.previousContent;
this.editor.cursorPos = this.pos;
}
}
class DeleteTextCommand implements Command {
description: string;
private deletedText = "";
constructor(private editor: TextEditor, private start: number, private end: number) {
this.description = "Delete chars " + start + "-" + end;
}
execute(): void {
this.deletedText = this.editor.content.slice(this.start, this.end);
this.editor.content = this.editor.content.slice(0, this.start) + this.editor.content.slice(this.end);
this.editor.cursorPos = this.start;
}
undo(): void {
const before = this.editor.content.slice(0, this.start);
const after = this.editor.content.slice(this.start);
this.editor.content = before + this.deletedText + after;
this.editor.cursorPos = this.start + this.deletedText.length;
}
}
class CommandHistory {
private undoStack: Command[] = [];
private redoStack: Command[] = [];
execute(command: Command): void {
command.execute();
this.undoStack.push(command);
this.redoStack = []; // clear redo stack on new action
}
undo(): void {
const command = this.undoStack.pop();
if (command) {
command.undo();
this.redoStack.push(command);
}
}
redo(): void {
const command = this.redoStack.pop();
if (command) {
command.execute();
this.undoStack.push(command);
}
}
}
// Usage
const editor = new TextEditor();
const history = new CommandHistory();
history.execute(new InsertTextCommand(editor, "Hello ", 0));
history.execute(new InsertTextCommand(editor, "World", 6));
console.log(editor.content); // "Hello World"
history.undo();
console.log(editor.content); // "Hello "
history.redo();
console.log(editor.content); // "Hello World"Python
# Command — Python
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self) -> None: ...
@abstractmethod
def undo(self) -> None: ...
class TextEditor:
def __init__(self):
self.content = ""
class InsertTextCommand(Command):
def __init__(self, editor: TextEditor, text: str, pos: int):
self._editor = editor
self._text = text
self._pos = pos
self._prev = ""
def execute(self) -> None:
self._prev = self._editor.content
before = self._editor.content[:self._pos]
after = self._editor.content[self._pos:]
self._editor.content = before + self._text + after
def undo(self) -> None:
self._editor.content = self._prev
class CommandHistory:
def __init__(self):
self._undo_stack: list[Command] = []
self._redo_stack: list[Command] = []
def execute(self, cmd: Command) -> None:
cmd.execute()
self._undo_stack.append(cmd)
self._redo_stack.clear()
def undo(self) -> None:
if self._undo_stack:
cmd = self._undo_stack.pop()
cmd.undo()
self._redo_stack.append(cmd)
# Usage
editor = TextEditor()
history = CommandHistory()
history.execute(InsertTextCommand(editor, "Hello ", 0))
history.execute(InsertTextCommand(editor, "World", 6))
print(editor.content) # Hello World
history.undo()
print(editor.content) # Hello 3.4 State Pattern
The State pattern allows an object to alter its behavior when its internal state changes, appearing to modify its class. It encapsulates state-specific behavior in separate state objects, eliminating large conditional statements.
TypeScript
// State — TypeScript (Order Processing)
interface OrderState {
name: string;
next(order: Order): void;
cancel(order: Order): void;
}
class Order {
state: OrderState;
id: string;
constructor(id: string) {
this.id = id;
this.state = new PendingState();
console.log("Order " + id + " created [" + this.state.name + "]");
}
next(): void { this.state.next(this); }
cancel(): void { this.state.cancel(this); }
transitionTo(state: OrderState): void {
console.log("Order " + this.id + ": " + this.state.name + " -> " + state.name);
this.state = state;
}
}
class PendingState implements OrderState {
name = "Pending";
next(order: Order): void {
order.transitionTo(new PaidState());
}
cancel(order: Order): void {
order.transitionTo(new CancelledState());
}
}
class PaidState implements OrderState {
name = "Paid";
next(order: Order): void {
order.transitionTo(new ShippedState());
}
cancel(order: Order): void {
console.log("Order " + order.id + ": refund initiated");
order.transitionTo(new CancelledState());
}
}
class ShippedState implements OrderState {
name = "Shipped";
next(order: Order): void {
order.transitionTo(new DeliveredState());
}
cancel(order: Order): void {
console.log("Order " + order.id + ": cannot cancel — already shipped");
}
}
class DeliveredState implements OrderState {
name = "Delivered";
next(order: Order): void {
console.log("Order " + order.id + ": already delivered — no further transitions");
}
cancel(order: Order): void {
console.log("Order " + order.id + ": cannot cancel — already delivered");
}
}
class CancelledState implements OrderState {
name = "Cancelled";
next(order: Order): void {
console.log("Order " + order.id + ": cancelled — no transitions allowed");
}
cancel(order: Order): void {
console.log("Order " + order.id + ": already cancelled");
}
}
// Usage
const order = new Order("ORD-001");
order.next(); // Pending -> Paid
order.next(); // Paid -> Shipped
order.cancel(); // cannot cancel — already shipped
order.next(); // Shipped -> DeliveredPython
# State — Python (Order Processing)
from abc import ABC, abstractmethod
class OrderState(ABC):
name: str
@abstractmethod
def next(self, order: "Order") -> None: ...
@abstractmethod
def cancel(self, order: "Order") -> None: ...
class Order:
def __init__(self, order_id: str):
self.id = order_id
self.state: OrderState = PendingState()
def next(self) -> None:
self.state.next(self)
def cancel(self) -> None:
self.state.cancel(self)
def transition_to(self, state: OrderState) -> None:
print(f"Order {self.id}: {self.state.name} -> {state.name}")
self.state = state
class PendingState(OrderState):
name = "Pending"
def next(self, order: Order) -> None:
order.transition_to(PaidState())
def cancel(self, order: Order) -> None:
order.transition_to(CancelledState())
class PaidState(OrderState):
name = "Paid"
def next(self, order: Order) -> None:
order.transition_to(ShippedState())
def cancel(self, order: Order) -> None:
print(f"Order {order.id}: refund initiated")
order.transition_to(CancelledState())
class ShippedState(OrderState):
name = "Shipped"
def next(self, order: Order) -> None:
order.transition_to(DeliveredState())
def cancel(self, order: Order) -> None:
print(f"Order {order.id}: cannot cancel — already shipped")
class DeliveredState(OrderState):
name = "Delivered"
def next(self, order: Order) -> None:
print(f"Order {order.id}: already delivered")
def cancel(self, order: Order) -> None:
print(f"Order {order.id}: cannot cancel — delivered")
class CancelledState(OrderState):
name = "Cancelled"
def next(self, order: Order) -> None:
print(f"Order {order.id}: cancelled — no transitions")
def cancel(self, order: Order) -> None:
print(f"Order {order.id}: already cancelled")
# Usage
order = Order("ORD-001")
order.next() # Pending -> Paid
order.next() # Paid -> Shipped
order.next() # Shipped -> Delivered4. Pattern Selection Guide
Choosing the right design pattern is as important as implementing it correctly. The table below helps you make quick decisions based on common scenarios.
| Scenario | Recommended Pattern | Why |
|---|---|---|
| Create objects based on config | Factory | Decouple creation logic, easy to extend |
| Build objects with many optional params | Builder | Readable, avoids constructor bloat |
| Integrate legacy or third-party libs | Adapter | Convert interfaces without modifying existing code |
| Combine behaviors dynamically (log+retry+cache) | Decorator | Stack features at runtime, more flexible than inheritance |
| Control access to expensive resources | Proxy | Lazy loading, caching, permission checks |
| Simplify complex APIs or subsystems | Facade | Provide clean entry point, reduce coupling |
| Event-driven notification | Observer | Decouple publishers and subscribers |
| Switch algorithms at runtime | Strategy | Eliminate conditional branches, easy to add new algorithms |
| Undo/redo or task queues | Command | Objectify requests, support logging and rollback |
| Object behavior varies with state | State | Eliminate if/switch chains for state checks |
5. Design Patterns and SOLID Principles
Design patterns do not exist in a vacuum — they are closely tied to the SOLID principles. Understanding this relationship helps you choose the right pattern at the right time.
| SOLID Principle | Related Patterns | Connection |
|---|---|---|
| SRP | Command, Strategy, State | Separate responsibilities into individual classes |
| OCP | Decorator, Strategy, Observer | Extend behavior without modifying existing code |
| LSP | Factory, Adapter | Subclasses/adapters can replace parent/target interfaces |
| ISP | Adapter, Facade | Provide lean interfaces without forcing unneeded methods |
| DIP | Factory, Strategy, Observer | Depend on abstractions, not concretions |
6. Common Anti-Patterns and Pitfalls
Using design patterns correctly requires judgment. Here are common mistakes developers make.
- Over-engineering: Do not use Factory when there is only one implementation, or Command when undo is not needed
- Singleton abuse: Global state makes testing hard; prefer dependency injection
- Pattern stacking: Do not apply three or four patterns to the same problem; keep it simple
- Ignoring language features: Python first-class functions can replace many Strategy/Command classes
- Premature abstraction: Do not introduce patterns before seeing duplication; follow the Rule of Three
Conclusion
Design patterns are time-tested solutions, but they are not silver bullets. The key is understanding what problem each pattern solves, when it applies, and what trade-offs it introduces. In TypeScript, interfaces and the type system provide compile-time guarantees for patterns. In Python, duck typing and Protocols make pattern implementations more concise. Regardless of language, remember: code is written for humans to read, and the greatest value of patterns lies in communication and maintainability.
Frequently Asked Questions
What are design patterns and why should I learn them?
Design patterns are reusable solutions to common software design problems. They provide a shared vocabulary for developers, improve code maintainability, and help you avoid reinventing the wheel. Learning them accelerates your ability to architect flexible, scalable systems and makes you a more effective collaborator.
What is the difference between Creational, Structural, and Behavioral patterns?
Creational patterns (Factory, Builder, Singleton) deal with object creation mechanisms. Structural patterns (Adapter, Decorator, Proxy, Facade) concern class and object composition. Behavioral patterns (Observer, Strategy, Command, State) focus on communication and responsibility between objects.
When should I use the Factory pattern vs the Builder pattern?
Use Factory when you need to create objects without specifying the exact class, typically when you have a family of related objects. Use Builder when constructing complex objects step by step, especially when the object has many optional parameters or requires a specific construction order.
Is Singleton an anti-pattern?
Singleton is sometimes considered an anti-pattern because it introduces global state, makes unit testing harder, and violates the Single Responsibility Principle. However, it is appropriate for genuinely shared resources like configuration managers, connection pools, or logging systems. In modern code, dependency injection often replaces Singleton.
How does the Decorator pattern differ from inheritance?
Decorator adds behavior to individual objects at runtime without affecting other instances, while inheritance extends entire classes at compile time. Decorator follows the Open/Closed Principle by allowing new behavior without modifying existing code. It also supports combining multiple behaviors dynamically, which is not possible with single inheritance.
What is the Strategy pattern and when should I use it?
The Strategy pattern defines a family of interchangeable algorithms, encapsulates each one, and makes them interchangeable at runtime. Use it when you have multiple ways to perform an operation (sorting, validation, pricing) and want to switch between them without conditional logic or modifying client code.
How do design patterns apply to TypeScript and Python differently?
TypeScript leverages interfaces, generics, and type guards to enforce pattern contracts at compile time. Python uses duck typing, protocols (PEP 544), ABC classes, and decorators. Python first-class functions often simplify patterns like Strategy and Command. TypeScript access modifiers (private, protected) enable stricter encapsulation.
Which design patterns are most commonly used in modern web development?
Observer (event systems, pub/sub, React state), Strategy (middleware, validation), Factory (API clients, component creation), Decorator (Python decorators, TypeScript decorators, higher-order components), Proxy (API gateways, caching layers), and Command (undo/redo, task queues) are the most prevalent in modern web and backend development.