- Published on
Design Patterns Modern Guide 2025: GoF Patterns + Modern Patterns in TypeScript/Python/Go
- Authors

- Name
- Youngju Kim
- @fjvbn20031
Table of Contents
1. Why Design Patterns Still Matter in 2025
The 23 design patterns published by the Gang of Four (GoF) in 1994 remain the bedrock of software design over 30 years later. Languages have evolved and paradigms have shifted, but the fundamental problems patterns solve have not changed.
Why patterns remain important in modern languages:
- Shared vocabulary — saying "let's apply the Strategy pattern here" gives the entire team instant understanding
- Proven solutions — design approaches validated across millions of projects over decades
- Framework comprehension — every major framework (Spring, NestJS, Django) is built on patterns
- Refactoring guide — when you spot a code smell, patterns show which direction to refactor
However, blindly applying patterns everywhere is counterproductive. Patterns are tools to apply when a problem exists, not rituals to make code more complex.
GoF 23 Pattern Classification
Creational (5): Object creation mechanisms
├── Factory Method
├── Abstract Factory
├── Builder
├── Singleton
└── Prototype
Structural (7): Object composition/structuring
├── Adapter
├── Bridge
├── Composite
├── Decorator
├── Facade
├── Flyweight
└── Proxy
Behavioral (11): Object interactions
├── Chain of Responsibility
├── Command
├── Iterator
├── Mediator
├── Memento
├── Observer
├── State
├── Strategy
├── Template Method
├── Visitor
└── Interpreter
This guide covers the 15 most commonly used GoF patterns + 5 modern patterns implemented in TypeScript, Python, and Go.
2. SOLID Principles: The Foundation of Patterns
Before diving into design patterns, let us examine the SOLID principles. Most patterns are concrete implementations of these principles.
2.1 SRP — Single Responsibility Principle
A class should have only one reason to change.
// Bad: Mixed responsibilities
class UserService {
createUser(data: UserData): User { /* ... */ }
sendWelcomeEmail(user: User): void { /* ... */ }
generateReport(users: User[]): PDF { /* ... */ }
}
// Good: Separated responsibilities
class UserService {
createUser(data: UserData): User { /* ... */ }
}
class EmailService {
sendWelcomeEmail(user: User): void { /* ... */ }
}
class ReportService {
generateReport(users: User[]): PDF { /* ... */ }
}
2.2 OCP — Open/Closed Principle
Open for extension, closed for modification. The Strategy, Decorator, and Observer patterns realize this principle.
# Bad: Must modify code for each new discount type
def calculate_discount(order, discount_type):
if discount_type == "percentage":
return order.total * 0.1
elif discount_type == "fixed":
return 10.0
# Must modify here to add new discount
# Good: Just add new discount types
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, order) -> float:
pass
class PercentageDiscount(DiscountStrategy):
def __init__(self, rate: float):
self.rate = rate
def calculate(self, order) -> float:
return order.total * self.rate
class FixedDiscount(DiscountStrategy):
def __init__(self, amount: float):
self.amount = amount
def calculate(self, order) -> float:
return self.amount
2.3 LSP — Liskov Substitution Principle
Subtypes must be substitutable for their base types. This is why a Square should not inherit from Rectangle.
2.4 ISP — Interface Segregation Principle
Clients should not be forced to depend on methods they do not use.
// Bad: Monolithic interface
type Worker interface {
Work()
Eat()
Sleep()
}
// Good: Segregated interfaces
type Workable interface {
Work()
}
type Eatable interface {
Eat()
}
// Go makes interface composition natural
type HumanWorker interface {
Workable
Eatable
}
2.5 DIP — Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions. This is the theoretical basis for Dependency Injection.
3. Creational Patterns
3.1 Factory Method Pattern
Problem: When object creation logic is embedded directly in client code, every creation site must be modified when adding new types.
Solution: Delegate object creation to subclasses or factory functions.
// TypeScript Factory Method
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[Console] ${message}`);
}
}
class FileLogger implements Logger {
log(message: string): void {
console.log(`[File] ${message}`);
}
}
class CloudLogger implements Logger {
log(message: string): void {
console.log(`[Cloud] ${message}`);
}
}
// Factory
type LoggerType = "console" | "file" | "cloud";
function createLogger(type: LoggerType): Logger {
const loggers: Record<LoggerType, () => Logger> = {
console: () => new ConsoleLogger(),
file: () => new FileLogger(),
cloud: () => new CloudLogger(),
};
const factory = loggers[type];
if (!factory) {
throw new Error(`Unknown logger type: ${type}`);
}
return factory();
}
// Usage
const logger = createLogger("cloud");
logger.log("Application started");
# Python Factory Method with registry
from abc import ABC, abstractmethod
from enum import Enum
class NotificationType(Enum):
EMAIL = "email"
SMS = "sms"
PUSH = "push"
SLACK = "slack"
class Notification(ABC):
@abstractmethod
def send(self, recipient: str, message: str) -> bool:
pass
class EmailNotification(Notification):
def send(self, recipient: str, message: str) -> bool:
print(f"Sending email to {recipient}: {message}")
return True
class SMSNotification(Notification):
def send(self, recipient: str, message: str) -> bool:
print(f"Sending SMS to {recipient}: {message}")
return True
class PushNotification(Notification):
def send(self, recipient: str, message: str) -> bool:
print(f"Sending push to {recipient}: {message}")
return True
class SlackNotification(Notification):
def send(self, recipient: str, message: str) -> bool:
print(f"Sending Slack to {recipient}: {message}")
return True
# Factory with registry pattern
class NotificationFactory:
_registry: dict[NotificationType, type[Notification]] = {
NotificationType.EMAIL: EmailNotification,
NotificationType.SMS: SMSNotification,
NotificationType.PUSH: PushNotification,
NotificationType.SLACK: SlackNotification,
}
@classmethod
def register(cls, ntype: NotificationType, klass: type[Notification]):
cls._registry[ntype] = klass
@classmethod
def create(cls, ntype: NotificationType) -> Notification:
klass = cls._registry.get(ntype)
if not klass:
raise ValueError(f"Unknown notification type: {ntype}")
return klass()
# Usage
notifier = NotificationFactory.create(NotificationType.SLACK)
notifier.send("team-channel", "Deploy complete!")
When to use: When the type of object to create must be determined at runtime, when you want to centralize creation logic.
When NOT to use: When there is only one type with no chance of change. Unnecessary abstraction only adds complexity.
3.2 Abstract Factory Pattern
Problem: Related object families must be created consistently.
Solution: Abstract the factories for related objects.
// TypeScript Abstract Factory — UI Theme System
interface Button {
render(): string;
}
interface Input {
render(): string;
}
interface Modal {
render(): string;
}
// Abstract Factory
interface UIFactory {
createButton(label: string): Button;
createInput(placeholder: string): Input;
createModal(title: string): Modal;
}
// Light Theme Implementation
class LightButton implements Button {
constructor(private label: string) {}
render() { return `<button class="bg-white text-black">${this.label}</button>`; }
}
class LightInput implements Input {
constructor(private placeholder: string) {}
render() { return `<input class="border-gray-300" placeholder="${this.placeholder}" />`; }
}
class LightModal implements Modal {
constructor(private title: string) {}
render() { return `<div class="bg-white shadow-lg">${this.title}</div>`; }
}
class LightThemeFactory implements UIFactory {
createButton(label: string) { return new LightButton(label); }
createInput(placeholder: string) { return new LightInput(placeholder); }
createModal(title: string) { return new LightModal(title); }
}
// Dark Theme Implementation
class DarkButton implements Button {
constructor(private label: string) {}
render() { return `<button class="bg-gray-800 text-white">${this.label}</button>`; }
}
class DarkInput implements Input {
constructor(private placeholder: string) {}
render() { return `<input class="border-gray-600 bg-gray-700" placeholder="${this.placeholder}" />`; }
}
class DarkModal implements Modal {
constructor(private title: string) {}
render() { return `<div class="bg-gray-900 text-white">${this.title}</div>`; }
}
class DarkThemeFactory implements UIFactory {
createButton(label: string) { return new DarkButton(label); }
createInput(placeholder: string) { return new DarkInput(placeholder); }
createModal(title: string) { return new DarkModal(title); }
}
// Usage — theme switching in one line
function buildUI(factory: UIFactory) {
const btn = factory.createButton("Submit");
const input = factory.createInput("Enter name...");
const modal = factory.createModal("Settings");
return { btn, input, modal };
}
const ui = buildUI(new DarkThemeFactory());
3.3 Builder Pattern
Problem: Objects with many constructor parameters become unreadable (Telescoping Constructor problem).
Solution: Provide a builder that constructs the object step by step.
// TypeScript Builder
interface HttpRequest {
method: string;
url: string;
headers: Record<string, string>;
body?: string;
timeout: number;
retries: number;
}
class HttpRequestBuilder {
private request: Partial<HttpRequest> = {
method: "GET",
headers: {},
timeout: 30000,
retries: 0,
};
setMethod(method: string): this {
this.request.method = method;
return this;
}
setUrl(url: string): this {
this.request.url = url;
return this;
}
addHeader(key: string, value: string): this {
this.request.headers![key] = value;
return this;
}
setBody(body: string): this {
this.request.body = body;
return this;
}
setTimeout(ms: number): this {
this.request.timeout = ms;
return this;
}
setRetries(count: number): this {
this.request.retries = count;
return this;
}
build(): HttpRequest {
if (!this.request.url) {
throw new Error("URL is required");
}
return this.request as HttpRequest;
}
}
// Usage: Highly readable
const request = new HttpRequestBuilder()
.setMethod("POST")
.setUrl("https://api.example.com/users")
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer token123")
.setBody(JSON.stringify({ name: "John" }))
.setTimeout(5000)
.setRetries(3)
.build();
# Python Builder — SQL query builder
from dataclasses import dataclass, field
from typing import Optional
@dataclass(frozen=True)
class QueryConfig:
table: str
columns: list[str] = field(default_factory=lambda: ["*"])
conditions: list[str] = field(default_factory=list)
order_by: Optional[str] = None
limit: Optional[int] = None
offset: int = 0
class QueryBuilder:
def __init__(self, table: str):
self._table = table
self._columns: list[str] = ["*"]
self._conditions: list[str] = []
self._order_by: Optional[str] = None
self._limit: Optional[int] = None
self._offset: int = 0
def select(self, *columns: str) -> "QueryBuilder":
self._columns = list(columns)
return self
def where(self, condition: str) -> "QueryBuilder":
self._conditions.append(condition)
return self
def order_by(self, column: str, desc: bool = False) -> "QueryBuilder":
direction = "DESC" if desc else "ASC"
self._order_by = 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) -> QueryConfig:
return QueryConfig(
table=self._table,
columns=self._columns,
conditions=self._conditions,
order_by=self._order_by,
limit=self._limit,
offset=self._offset,
)
def to_sql(self) -> str:
cols = ", ".join(self._columns)
sql = f"SELECT {cols} FROM {self._table}"
if self._conditions:
sql += " WHERE " + " AND ".join(self._conditions)
if self._order_by:
sql += f" ORDER BY {self._order_by}"
if self._limit:
sql += f" LIMIT {self._limit}"
if self._offset:
sql += f" OFFSET {self._offset}"
return sql
# Usage
query = (
QueryBuilder("users")
.select("id", "name", "email")
.where("status = 'active'")
.where("age > 18")
.order_by("created_at", desc=True)
.limit(20)
.offset(40)
)
print(query.to_sql())
# SELECT id, name, email FROM users WHERE status = 'active' AND age > 18 ORDER BY created_at DESC LIMIT 20 OFFSET 40
3.4 Singleton Pattern
Problem: Only one instance of a particular class should exist.
Solution: Restrict construction and provide a global access point.
// TypeScript Singleton — Modern approach
class ConfigManager {
private static instance: ConfigManager | null = null;
private config: Map<string, unknown> = new Map();
private constructor() {
// private constructor — cannot new from outside
}
static getInstance(): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
get<T>(key: string): T | undefined {
return this.config.get(key) as T | undefined;
}
set(key: string, value: unknown): void {
this.config.set(key, value);
}
}
// Usage
const config = ConfigManager.getInstance();
config.set("database.host", "localhost");
// Go Singleton — using sync.Once
package config
import "sync"
type AppConfig struct {
DatabaseURL string
RedisURL string
Port int
}
var (
instance *AppConfig
once sync.Once
)
func GetConfig() *AppConfig {
once.Do(func() {
instance = &AppConfig{
DatabaseURL: "postgres://localhost:5432/app",
RedisURL: "redis://localhost:6379",
Port: 8080,
}
})
return instance
}
Caution: Singletons create global state and make testing difficult. Managing as "single scope" in a DI container is the more modern approach.
3.5 Prototype Pattern
Problem: Creating complex objects from scratch is expensive.
Solution: Clone existing objects to create new ones.
// TypeScript Prototype
interface Cloneable<T> {
clone(): T;
}
class GameCharacter implements Cloneable<GameCharacter> {
constructor(
public name: string,
public health: number,
public attack: number,
public defense: number,
public skills: string[],
public inventory: Map<string, number>,
) {}
clone(): GameCharacter {
return new GameCharacter(
this.name,
this.health,
this.attack,
this.defense,
[...this.skills],
new Map(this.inventory),
);
}
}
// Prototype registry
const archetypes = {
warrior: new GameCharacter("Warrior", 200, 30, 50, ["Slash", "Shield"], new Map([["potion", 3]])),
mage: new GameCharacter("Mage", 100, 60, 20, ["Fireball", "Heal"], new Map([["mana_potion", 5]])),
};
// Clone and customize
const myWarrior = archetypes.warrior.clone();
myWarrior.name = "Aragorn";
myWarrior.skills.push("Charge");
4. Structural Patterns
4.1 Adapter Pattern
Problem: You need to use existing classes whose interfaces do not match.
Solution: Place a translation layer (adapter) in between.
// TypeScript Adapter — External payment library integration
// Our system's payment interface
interface PaymentProcessor {
charge(amount: number, currency: string): Promise<PaymentResult>;
refund(transactionId: string, amount: number): Promise<RefundResult>;
}
interface PaymentResult {
success: boolean;
transactionId: string;
}
interface RefundResult {
success: boolean;
refundId: string;
}
// External library (cannot modify)
class StripeSDK {
async createCharge(params: {
amount_cents: number;
currency: string;
}): Promise<{ id: string; status: string }> {
return { id: "ch_123", status: "succeeded" };
}
async createRefund(params: {
charge: string;
amount_cents: number;
}): Promise<{ id: string; status: string }> {
return { id: "re_456", status: "succeeded" };
}
}
// Adapter
class StripeAdapter implements PaymentProcessor {
private stripe: StripeSDK;
constructor() {
this.stripe = new StripeSDK();
}
async charge(amount: number, currency: string): Promise<PaymentResult> {
const result = await this.stripe.createCharge({
amount_cents: Math.round(amount * 100),
currency: currency.toLowerCase(),
});
return {
success: result.status === "succeeded",
transactionId: result.id,
};
}
async refund(transactionId: string, amount: number): Promise<RefundResult> {
const result = await this.stripe.createRefund({
charge: transactionId,
amount_cents: Math.round(amount * 100),
});
return {
success: result.status === "succeeded",
refundId: result.id,
};
}
}
// Usage — our code only needs to know PaymentProcessor
const processor: PaymentProcessor = new StripeAdapter();
const result = await processor.charge(29.99, "USD");
4.2 Decorator Pattern
Problem: You need to dynamically add responsibilities to objects, but subclassing is inflexible.
Solution: Wrap with wrapper objects to add functionality.
# Python Decorator — Composable logging, caching, retry
import functools
import time
import logging
from typing import Callable, TypeVar, ParamSpec
P = ParamSpec("P")
R = TypeVar("R")
def with_logging(func: Callable[P, R]) -> Callable[P, R]:
"""Log function calls"""
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
logging.info(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
logging.info(f"{func.__name__} returned in {elapsed:.3f}s")
return result
return wrapper
def with_retry(max_retries: int = 3, delay: float = 1.0):
"""Retry on failure"""
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
last_error: Exception | None = None
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e
logging.warning(
f"Attempt {attempt + 1}/{max_retries} failed: {e}"
)
if attempt < max_retries - 1:
time.sleep(delay * (2 ** attempt))
raise last_error # type: ignore
return wrapper
return decorator
def with_cache(ttl_seconds: int = 300):
"""Cache results"""
cache: dict[str, tuple[float, object]] = {}
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
key = f"{args}-{kwargs}"
if key in cache:
cached_time, cached_value = cache[key]
if time.time() - cached_time < ttl_seconds:
return cached_value # type: ignore
result = func(*args, **kwargs)
cache[key] = (time.time(), result)
return result
return wrapper
return decorator
# Composing decorators — applied from outer to inner
@with_logging
@with_retry(max_retries=3, delay=0.5)
@with_cache(ttl_seconds=60)
def fetch_user_profile(user_id: int) -> dict:
"""Fetch user profile from external API"""
import requests
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()
4.3 Facade Pattern
Problem: Using a complex subsystem requires knowledge of too many classes and methods.
Solution: Create a Facade that provides a simplified interface.
// TypeScript Facade — Order processing system
class InventoryService {
checkStock(productId: string): boolean { return true; }
reserveStock(productId: string, qty: number): void { /* ... */ }
}
class PaymentService {
processPayment(userId: string, amount: number): string { return "txn_789"; }
refundPayment(txnId: string): void { /* ... */ }
}
class ShippingService {
calculateShipping(address: string): number { return 5.99; }
createShipment(orderId: string, address: string): string { return "ship_101"; }
}
class NotificationService {
sendOrderConfirmation(email: string, orderId: string): void { /* ... */ }
sendShippingNotification(email: string, trackingId: string): void { /* ... */ }
}
// Facade — complex process in a simple method
class OrderFacade {
private inventory = new InventoryService();
private payment = new PaymentService();
private shipping = new ShippingService();
private notification = new NotificationService();
async placeOrder(order: {
userId: string;
email: string;
productId: string;
quantity: number;
address: string;
amount: number;
}): Promise<{ orderId: string; trackingId: string }> {
// 1. Check stock
if (!this.inventory.checkStock(order.productId)) {
throw new Error("Out of stock");
}
// 2. Reserve stock
this.inventory.reserveStock(order.productId, order.quantity);
// 3. Process payment
const shippingCost = this.shipping.calculateShipping(order.address);
const txnId = this.payment.processPayment(
order.userId,
order.amount + shippingCost,
);
// 4. Create shipment
const orderId = `ORD-${Date.now()}`;
const trackingId = this.shipping.createShipment(orderId, order.address);
// 5. Send notification
this.notification.sendOrderConfirmation(order.email, orderId);
return { orderId, trackingId };
}
}
// Usage — client only needs the Facade
const orderService = new OrderFacade();
const result = await orderService.placeOrder({
userId: "user_1",
email: "user@example.com",
productId: "prod_42",
quantity: 1,
address: "Seoul, Korea",
amount: 49.99,
});
4.4 Proxy Pattern
Problem: You need to control access to an object (lazy loading, access control, logging, etc.).
Solution: Provide a proxy that wraps the real object.
// TypeScript Proxy — Lazy loading + access control
interface Database {
query(sql: string): Promise<unknown[]>;
execute(sql: string): Promise<void>;
}
class PostgresDatabase implements Database {
private pool: unknown;
constructor(connectionString: string) {
console.log("Establishing DB connection pool...");
this.pool = {};
}
async query(sql: string): Promise<unknown[]> {
console.log(`Executing query: ${sql}`);
return [];
}
async execute(sql: string): Promise<void> {
console.log(`Executing: ${sql}`);
}
}
class DatabaseProxy implements Database {
private db: PostgresDatabase | null = null;
private connectionString: string;
private userRole: string;
constructor(connectionString: string, userRole: string) {
this.connectionString = connectionString;
this.userRole = userRole;
// DB connection not yet established — lazy loading
}
private getDb(): PostgresDatabase {
if (!this.db) {
this.db = new PostgresDatabase(this.connectionString);
}
return this.db;
}
async query(sql: string): Promise<unknown[]> {
console.log(`[Proxy] Query requested by role: ${this.userRole}`);
return this.getDb().query(sql);
}
async execute(sql: string): Promise<void> {
if (this.userRole !== "admin") {
throw new Error("Only admin can execute write operations");
}
console.log(`[Proxy] Execute requested by role: ${this.userRole}`);
return this.getDb().execute(sql);
}
}
4.5 Composite Pattern
Problem: You want to treat individual objects and compositions of objects uniformly in tree structures.
Solution: Single objects and composites implement the same interface.
# Python Composite — File system
from abc import ABC, abstractmethod
class FileSystemItem(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def get_size(self) -> int:
pass
@abstractmethod
def display(self, indent: int = 0) -> str:
pass
class File(FileSystemItem):
def __init__(self, name: str, size: int):
super().__init__(name)
self.size = size
def get_size(self) -> int:
return self.size
def display(self, indent: int = 0) -> str:
return " " * indent + f"File: {self.name} ({self.size} bytes)"
class Directory(FileSystemItem):
def __init__(self, name: str):
super().__init__(name)
self.children: list[FileSystemItem] = []
def add(self, item: FileSystemItem) -> "Directory":
self.children.append(item)
return self
def get_size(self) -> int:
return sum(child.get_size() for child in self.children)
def display(self, indent: int = 0) -> str:
lines = [" " * indent + f"Dir: {self.name} ({self.get_size()} bytes)"]
for child in self.children:
lines.append(child.display(indent + 2))
return "\n".join(lines)
# Usage
root = Directory("project")
src = Directory("src")
src.add(File("main.ts", 2048))
src.add(File("utils.ts", 1024))
root.add(src)
root.add(File("package.json", 512))
root.add(File("README.md", 256))
print(root.display())
print(f"Total size: {root.get_size()} bytes")
5. Behavioral Patterns
5.1 Strategy Pattern
Problem: Algorithms need to be interchangeable at runtime. if-else or switch chains keep growing.
Solution: Encapsulate algorithms and make them interchangeable.
// TypeScript Strategy — Pricing strategies
interface PricingStrategy {
calculatePrice(basePrice: number, quantity: number): number;
getName(): string;
}
class RegularPricing implements PricingStrategy {
calculatePrice(basePrice: number, quantity: number): number {
return basePrice * quantity;
}
getName() { return "Regular"; }
}
class BulkPricing implements PricingStrategy {
calculatePrice(basePrice: number, quantity: number): number {
if (quantity >= 100) return basePrice * quantity * 0.7;
if (quantity >= 50) return basePrice * quantity * 0.8;
if (quantity >= 10) return basePrice * quantity * 0.9;
return basePrice * quantity;
}
getName() { return "Bulk"; }
}
class SubscriptionPricing implements PricingStrategy {
constructor(private discountRate: number) {}
calculatePrice(basePrice: number, quantity: number): number {
return basePrice * quantity * (1 - this.discountRate);
}
getName() { return "Subscription"; }
}
// Context
class ShoppingCart {
private items: Array<{ name: string; price: number; quantity: number }> = [];
private strategy: PricingStrategy = new RegularPricing();
setPricingStrategy(strategy: PricingStrategy): void {
this.strategy = strategy;
console.log(`Pricing strategy set to: ${strategy.getName()}`);
}
addItem(name: string, price: number, quantity: number): void {
this.items.push({ name, price, quantity });
}
calculateTotal(): number {
return this.items.reduce(
(total, item) => total + this.strategy.calculatePrice(item.price, item.quantity),
0,
);
}
}
// Usage
const cart = new ShoppingCart();
cart.addItem("Widget", 10, 100);
cart.setPricingStrategy(new RegularPricing());
console.log(`Regular: $${cart.calculateTotal()}`); // 1000
cart.setPricingStrategy(new BulkPricing());
console.log(`Bulk: $${cart.calculateTotal()}`); // 700
cart.setPricingStrategy(new SubscriptionPricing(0.15));
console.log(`Subscription: $${cart.calculateTotal()}`); // 850
5.2 Observer Pattern
Problem: Other objects need to automatically detect state changes in one object.
Solution: Implement a publish-subscribe (Pub/Sub) mechanism.
// TypeScript Observer — Type-safe event system
type EventMap = {
"user:created": { userId: string; email: string };
"user:updated": { userId: string; changes: Record<string, unknown> };
"order:placed": { orderId: string; userId: string; total: number };
"order:shipped": { orderId: string; trackingNumber: string };
};
type EventHandler<T> = (data: T) => void | Promise<void>;
class EventEmitter {
private handlers = new Map<string, Set<EventHandler<any>>>();
on<K extends keyof EventMap>(event: K, handler: EventHandler<EventMap[K]>): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
// Return unsubscribe function
return () => {
this.handlers.get(event)?.delete(handler);
};
}
async emit<K extends keyof EventMap>(event: K, data: EventMap[K]): Promise<void> {
const eventHandlers = this.handlers.get(event);
if (!eventHandlers) return;
const promises = Array.from(eventHandlers).map((handler) =>
Promise.resolve(handler(data))
);
await Promise.allSettled(promises);
}
}
// Usage
const events = new EventEmitter();
events.on("user:created", async (data) => {
console.log(`Send welcome email to ${data.email}`);
});
events.on("user:created", async (data) => {
console.log(`Initialize user preferences for ${data.userId}`);
});
events.on("order:placed", async (data) => {
console.log(`Process payment for order ${data.orderId}: $${data.total}`);
});
await events.emit("user:created", { userId: "u_1", email: "user@example.com" });
await events.emit("order:placed", { orderId: "ord_1", userId: "u_1", total: 99.99 });
5.3 Command Pattern
Problem: You need to encapsulate requests as objects for queuing, logging, and undo support.
Solution: Turn each operation into a Command object.
# Python Command — Text editor with Undo/Redo
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
@abstractmethod
def describe(self) -> str:
pass
class TextEditor:
def __init__(self):
self.content: list[str] = []
self._cursor: int = 0
@property
def text(self) -> str:
return "".join(self.content)
def insert_at(self, position: int, text: str) -> None:
for i, char in enumerate(text):
self.content.insert(position + i, char)
def delete_range(self, start: int, length: int) -> str:
deleted = self.content[start:start + length]
del self.content[start:start + length]
return "".join(deleted)
class InsertCommand(Command):
def __init__(self, editor: TextEditor, position: int, text: str):
self.editor = editor
self.position = position
self.text = text
def execute(self) -> None:
self.editor.insert_at(self.position, self.text)
def undo(self) -> None:
self.editor.delete_range(self.position, len(self.text))
def describe(self) -> str:
return f"Insert '{self.text}' at position {self.position}"
class DeleteCommand(Command):
def __init__(self, editor: TextEditor, position: int, length: int):
self.editor = editor
self.position = position
self.length = length
self.deleted_text: str = ""
def execute(self) -> None:
self.deleted_text = self.editor.delete_range(self.position, self.length)
def undo(self) -> None:
self.editor.insert_at(self.position, self.deleted_text)
def describe(self) -> str:
return f"Delete {self.length} chars at position {self.position}"
@dataclass
class CommandHistory:
undo_stack: list[Command] = field(default_factory=list)
redo_stack: list[Command] = field(default_factory=list)
def execute(self, command: Command) -> None:
command.execute()
self.undo_stack.append(command)
self.redo_stack.clear()
def undo(self) -> str | None:
if not self.undo_stack:
return None
command = self.undo_stack.pop()
command.undo()
self.redo_stack.append(command)
return command.describe()
def redo(self) -> str | None:
if not self.redo_stack:
return None
command = self.redo_stack.pop()
command.execute()
self.undo_stack.append(command)
return command.describe()
# Usage
editor = TextEditor()
history = CommandHistory()
history.execute(InsertCommand(editor, 0, "Hello World"))
print(editor.text) # "Hello World"
history.execute(InsertCommand(editor, 5, ","))
print(editor.text) # "Hello, World"
history.undo()
print(editor.text) # "Hello World"
history.redo()
print(editor.text) # "Hello, World"
5.4 State Pattern
Problem: Object behavior changes completely based on internal state, leading to massive if-else/switch blocks.
Solution: Separate each state into its own class.
// TypeScript State — Order state machine
interface OrderState {
name: string;
confirm(order: Order): void;
ship(order: Order): void;
deliver(order: Order): void;
cancel(order: Order): void;
}
class PendingState implements OrderState {
name = "Pending";
confirm(order: Order) { order.setState(new ConfirmedState()); }
ship(_order: Order) { throw new Error("Cannot ship a pending order"); }
deliver(_order: Order) { throw new Error("Cannot deliver a pending order"); }
cancel(order: Order) { order.setState(new CancelledState()); }
}
class ConfirmedState implements OrderState {
name = "Confirmed";
confirm(_order: Order) { throw new Error("Order already confirmed"); }
ship(order: Order) { order.setState(new ShippedState()); }
deliver(_order: Order) { throw new Error("Cannot deliver before shipping"); }
cancel(order: Order) { order.setState(new CancelledState()); }
}
class ShippedState implements OrderState {
name = "Shipped";
confirm(_order: Order) { throw new Error("Order already shipped"); }
ship(_order: Order) { throw new Error("Order already shipped"); }
deliver(order: Order) { order.setState(new DeliveredState()); }
cancel(_order: Order) { throw new Error("Cannot cancel a shipped order"); }
}
class DeliveredState implements OrderState {
name = "Delivered";
confirm() { throw new Error("Order already delivered"); }
ship() { throw new Error("Order already delivered"); }
deliver() { throw new Error("Order already delivered"); }
cancel() { throw new Error("Cannot cancel a delivered order"); }
}
class CancelledState implements OrderState {
name = "Cancelled";
confirm() { throw new Error("Order is cancelled"); }
ship() { throw new Error("Order is cancelled"); }
deliver() { throw new Error("Order is cancelled"); }
cancel() { throw new Error("Order already cancelled"); }
}
class Order {
private state: OrderState = new PendingState();
setState(state: OrderState): void {
console.log(`Order state: ${this.state.name} -> ${state.name}`);
this.state = state;
}
getState(): string { return this.state.name; }
confirm() { this.state.confirm(this); }
ship() { this.state.ship(this); }
deliver() { this.state.deliver(this); }
cancel() { this.state.cancel(this); }
}
// Usage
const order = new Order();
order.confirm(); // Pending -> Confirmed
order.ship(); // Confirmed -> Shipped
order.deliver(); // Shipped -> Delivered
5.5 Template Method Pattern
Problem: The algorithm skeleton is identical, but only certain steps differ.
Solution: Define the base algorithm in a parent class and let subclasses override the varying steps.
# Python Template Method — Data processing pipeline
from abc import ABC, abstractmethod
from typing import Any
import json
import csv
from io import StringIO
class DataProcessor(ABC):
"""Data processing pipeline — template method"""
def process(self, source: str) -> dict[str, Any]:
"""Template method: defines the processing flow"""
raw_data = self.extract(source)
validated = self.validate(raw_data)
transformed = self.transform(validated)
result = self.load(transformed)
self.notify(result)
return result
@abstractmethod
def extract(self, source: str) -> list[dict]:
"""Extract data — subclass implements"""
pass
def validate(self, data: list[dict]) -> list[dict]:
"""Default validation — override if needed"""
return [row for row in data if row]
@abstractmethod
def transform(self, data: list[dict]) -> list[dict]:
"""Transform data — subclass implements"""
pass
def load(self, data: list[dict]) -> dict[str, Any]:
"""Return results — default implementation"""
return {"records": len(data), "data": data}
def notify(self, result: dict[str, Any]) -> None:
"""Completion notification — optional override"""
print(f"Processed {result['records']} records")
class JSONProcessor(DataProcessor):
def extract(self, source: str) -> list[dict]:
return json.loads(source)
def transform(self, data: list[dict]) -> list[dict]:
return [{k.lower(): v for k, v in row.items()} for row in data]
class CSVProcessor(DataProcessor):
def extract(self, source: str) -> list[dict]:
reader = csv.DictReader(StringIO(source))
return list(reader)
def transform(self, data: list[dict]) -> list[dict]:
for row in data:
for key, value in row.items():
try:
row[key] = float(value)
except (ValueError, TypeError):
pass
return data
6. Modern Patterns
6.1 Repository Pattern
Separates data access logic from business logic.
// TypeScript Repository
interface Repository<T, ID> {
findById(id: ID): Promise<T | null>;
findAll(filter?: Partial<T>): Promise<T[]>;
create(entity: Omit<T, "id">): Promise<T>;
update(id: ID, data: Partial<T>): Promise<T>;
delete(id: ID): Promise<boolean>;
}
interface User {
id: string;
name: string;
email: string;
role: string;
createdAt: Date;
}
// In-Memory implementation — for testing
class InMemoryUserRepository implements Repository<User, string> {
private users = new Map<string, User>();
async findById(id: string): Promise<User | null> {
return this.users.get(id) || null;
}
async findAll(filter?: Partial<User>): Promise<User[]> {
let results = Array.from(this.users.values());
if (filter?.role) results = results.filter((u) => u.role === filter.role);
return results;
}
async create(data: Omit<User, "id">): Promise<User> {
const user: User = { id: crypto.randomUUID(), ...data } as User;
this.users.set(user.id, user);
return user;
}
async update(id: string, data: Partial<User>): Promise<User> {
const user = this.users.get(id);
if (!user) throw new Error("User not found");
const updated = { ...user, ...data };
this.users.set(id, updated);
return updated;
}
async delete(id: string): Promise<boolean> {
return this.users.delete(id);
}
}
6.2 CQRS (Command Query Responsibility Segregation)
Separates read (Query) and write (Command) models.
// TypeScript CQRS
// Command — Write
interface Command {
type: string;
}
interface CreateOrderCommand extends Command {
type: "CreateOrder";
userId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
}
interface CancelOrderCommand extends Command {
type: "CancelOrder";
orderId: string;
reason: string;
}
type OrderCommand = CreateOrderCommand | CancelOrderCommand;
// Command Handler
class OrderCommandHandler {
async handle(command: OrderCommand): Promise<void> {
switch (command.type) {
case "CreateOrder":
await this.createOrder(command);
break;
case "CancelOrder":
await this.cancelOrder(command);
break;
}
}
private async createOrder(cmd: CreateOrderCommand): Promise<void> {
const total = cmd.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
console.log(`Order created for user ${cmd.userId}, total: ${total}`);
}
private async cancelOrder(cmd: CancelOrderCommand): Promise<void> {
console.log(`Order ${cmd.orderId} cancelled: ${cmd.reason}`);
}
}
// Query — Read (separate optimized model)
interface OrderSummary {
orderId: string;
userName: string;
totalAmount: number;
status: string;
itemCount: number;
}
class OrderQueryService {
async getOrderSummary(orderId: string): Promise<OrderSummary | null> {
return null;
}
async getUserOrders(userId: string): Promise<OrderSummary[]> {
return [];
}
}
6.3 Circuit Breaker Pattern
Prevents failures in external services from cascading to the entire system.
// TypeScript Circuit Breaker
enum CircuitState {
CLOSED = "CLOSED",
OPEN = "OPEN",
HALF_OPEN = "HALF_OPEN",
}
class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED;
private failureCount: number = 0;
private lastFailureTime: number = 0;
private successCount: number = 0;
constructor(
private readonly failureThreshold: number = 5,
private readonly recoveryTimeout: number = 30000,
private readonly halfOpenMaxAttempts: number = 3,
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
this.state = CircuitState.HALF_OPEN;
this.successCount = 0;
console.log("Circuit: OPEN -> HALF_OPEN");
} else {
throw new Error("Circuit is OPEN - request blocked");
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.halfOpenMaxAttempts) {
this.state = CircuitState.CLOSED;
this.failureCount = 0;
console.log("Circuit: HALF_OPEN -> CLOSED");
}
} else {
this.failureCount = 0;
}
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.state === CircuitState.HALF_OPEN) {
this.state = CircuitState.OPEN;
console.log("Circuit: HALF_OPEN -> OPEN");
} else if (this.failureCount >= this.failureThreshold) {
this.state = CircuitState.OPEN;
console.log("Circuit: CLOSED -> OPEN");
}
}
getState(): CircuitState {
return this.state;
}
}
// Usage
const breaker = new CircuitBreaker(3, 10000);
async function callExternalApi(): Promise<string> {
try {
return await breaker.execute(async () => {
const res = await fetch("https://api.example.com/data");
if (!res.ok) throw new Error("API error");
return res.text();
});
} catch (err) {
console.log(`Fallback: ${(err as Error).message}`);
return "cached-data";
}
}
6.4 Specification Pattern
Encapsulates business rules into reusable objects.
# Python Specification
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Generic, TypeVar
T = TypeVar("T")
class Specification(ABC, Generic[T]):
@abstractmethod
def is_satisfied_by(self, candidate: T) -> bool:
pass
def and_spec(self, other: "Specification[T]") -> "Specification[T]":
return AndSpecification(self, other)
def or_spec(self, other: "Specification[T]") -> "Specification[T]":
return OrSpecification(self, other)
def not_spec(self) -> "Specification[T]":
return NotSpecification(self)
class AndSpecification(Specification[T]):
def __init__(self, left: Specification[T], right: Specification[T]):
self.left = left
self.right = right
def is_satisfied_by(self, candidate: T) -> bool:
return (self.left.is_satisfied_by(candidate)
and self.right.is_satisfied_by(candidate))
class OrSpecification(Specification[T]):
def __init__(self, left: Specification[T], right: Specification[T]):
self.left = left
self.right = right
def is_satisfied_by(self, candidate: T) -> bool:
return (self.left.is_satisfied_by(candidate)
or self.right.is_satisfied_by(candidate))
class NotSpecification(Specification[T]):
def __init__(self, spec: Specification[T]):
self.spec = spec
def is_satisfied_by(self, candidate: T) -> bool:
return not self.spec.is_satisfied_by(candidate)
# Usage
@dataclass
class Product:
name: str
price: float
category: str
in_stock: bool
rating: float
class InStockSpec(Specification[Product]):
def is_satisfied_by(self, product: Product) -> bool:
return product.in_stock
class PriceRangeSpec(Specification[Product]):
def __init__(self, min_price: float, max_price: float):
self.min_price = min_price
self.max_price = max_price
def is_satisfied_by(self, product: Product) -> bool:
return self.min_price <= product.price <= self.max_price
class HighRatedSpec(Specification[Product]):
def __init__(self, min_rating: float = 4.0):
self.min_rating = min_rating
def is_satisfied_by(self, product: Product) -> bool:
return product.rating >= self.min_rating
# Composing specifications
affordable_and_good = (
PriceRangeSpec(10, 50)
.and_spec(HighRatedSpec(4.5))
.and_spec(InStockSpec())
)
products = [
Product("Widget A", 25.0, "electronics", True, 4.7),
Product("Widget B", 15.0, "electronics", True, 3.2),
Product("Widget C", 35.0, "books", False, 4.8),
Product("Widget D", 45.0, "electronics", True, 4.9),
]
filtered = [p for p in products if affordable_and_good.is_satisfied_by(p)]
print([p.name for p in filtered]) # ['Widget A', 'Widget D']
6.5 Unit of Work Pattern
Groups changes across multiple repositories into a single transaction.
# Python Unit of Work
from contextlib import contextmanager
from typing import Generator
class UnitOfWork:
def __init__(self, session_factory):
self.session_factory = session_factory
@contextmanager
def transaction(self) -> Generator:
session = self.session_factory()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
# Usage (SQLAlchemy style)
class OrderService:
def __init__(self, uow: UnitOfWork):
self.uow = uow
def place_order(self, user_id: str, items: list[dict]) -> str:
with self.uow.transaction() as session:
order = Order(user_id=user_id, status="pending")
session.add(order)
for item in items:
order_item = OrderItem(
order_id=order.id,
product_id=item["product_id"],
quantity=item["quantity"],
)
session.add(order_item)
product = session.query(Product).get(item["product_id"])
product.stock -= item["quantity"]
return order.id
7. Anti-Patterns
Design patterns to avoid.
7.1 God Object
A massive class that knows and does everything. Completely violates SRP.
// Bad: God Object
class Application {
createUser() { /* ... */ }
deleteUser() { /* ... */ }
authenticateUser() { /* ... */ }
createOrder() { /* ... */ }
processPayment() { /* ... */ }
sendEmail() { /* ... */ }
renderTemplate() { /* ... */ }
logError() { /* ... */ }
logInfo() { /* ... */ }
getFromCache() { /* ... */ }
setToCache() { /* ... */ }
}
// This class can change for 10+ different reasons
Fix: Separate classes by responsibility and provide a unified interface with Facade.
7.2 Spaghetti Code
Tangled code where flow is hard to follow. Caused by callback hell, nested if-else, goto-like patterns.
7.3 Golden Hammer
Applying one tool (pattern) to every problem. "When you have a hammer, everything looks like a nail."
7.4 Premature Optimization
Making code complex by optimizing non-bottlenecks.
7.5 Copy-Paste Programming
Copying code with minor modifications. Can be solved with Template Method or Strategy.
8. Pattern Selection Guide
Problem Scenario Recommended Pattern
──────────────────────────────────────────────────────────────
Object creation is complex Builder
Choose one algorithm among many Strategy
Behavior changes based on state State
Integrate code with mismatched interfaces Adapter
Dynamically add functionality Decorator
Simplify a complex system Facade
Event-based communication needed Observer
Need to undo operations Command
Abstract data access Repository
Isolate external service failures Circuit Breaker
9. Quiz
Q1. What is the difference between Factory Method and Abstract Factory?
Factory Method overrides a method to create one product, while Abstract Factory creates a family of related products consistently. When you need to create buttons, inputs, and modals as a cohesive set (like a UI theme system), Abstract Factory is the right choice.
Q2. Strategy and State patterns have similar structures. What is the difference?
Both delegate behavior, but the intent differs. Strategy lets the client choose and swap algorithms externally, while State changes behavior automatically through internal state transitions. The client drives Strategy switching; state objects themselves drive State transitions.
Q3. What is the difference between Decorator and Proxy patterns?
Both are wrapper objects but with different purposes. Decorator adds functionality (decoration), while Proxy controls access. Decorators commonly nest multiple layers, while Proxies typically have one layer.
Q4. When should you use CQRS?
CQRS is suitable when read and write load patterns differ significantly. For example, when writes require complex business logic but reads are simple queries, each can be optimized independently. However, it significantly increases complexity, so it is overkill for simple CRUD applications.
Q5. What are the three states of a Circuit Breaker and their transition conditions?
CLOSED (normal): Requests pass through normally. Transitions to OPEN when failure count exceeds the threshold. OPEN (blocked): All requests fail immediately. Transitions to HALF_OPEN after recovery timeout elapses. HALF_OPEN (testing): Allows a limited number of requests. Transitions to CLOSED on success, back to OPEN on failure.
10. References
- Gamma, E. et al. — Design Patterns: Elements of Reusable Object-Oriented Software (GoF Book)
- Martin, R.C. — Clean Architecture: A Craftsman's Guide to Software Structure and Design
- Fowler, M. — Patterns of Enterprise Application Architecture
- Refactoring Guru — https://refactoring.guru/design-patterns
- Head First Design Patterns (2nd Edition, 2020)
- Martin, R.C. — SOLID Principles Explained
- Microsoft — Cloud Design Patterns — https://learn.microsoft.com/en-us/azure/architecture/patterns/
- Nygard, M. — Release It! Design and Deploy Production-Ready Software
- Evans, E. — Domain-Driven Design: Tackling Complexity in the Heart of Software
- Fowler, M. — https://martinfowler.com/articles/enterprisePatterns.html
- Go Design Patterns — https://github.com/tmrts/go-patterns
- TypeScript Design Patterns — https://github.com/torokmark/design_patterns_in_typescript
- Python Design Patterns — https://python-patterns.guide/