Skip to content

Split View: 디자인 패턴 현대적 가이드 2025: TypeScript/Python/Go로 배우는 GoF 패턴 + 현대 패턴

✨ Learn with Quiz
|

디자인 패턴 현대적 가이드 2025: TypeScript/Python/Go로 배우는 GoF 패턴 + 현대 패턴

목차

1. 왜 2025년에도 디자인 패턴이 중요한가

1994년 GoF(Gang of Four)가 발표한 23개 디자인 패턴은 30년이 지난 지금도 소프트웨어 설계의 근간을 이룬다. 언어가 진화하고 패러다임이 바뀌었지만, 패턴이 해결하는 근본적인 문제는 변하지 않았다.

현대 언어에서 패턴이 여전히 중요한 이유:

  • 공통 어휘 — "여기에 Strategy 패턴을 적용하자"라고 말하면 팀 전체가 즉시 이해한다
  • 검증된 해결책 — 수십 년간 수백만 프로젝트에서 검증된 설계 방법
  • 프레임워크 이해 — Spring, NestJS, Django 등 모든 주요 프레임워크가 패턴을 기반으로 설계되었다
  • 리팩토링 가이드 — 코드 스멜을 발견했을 때 어떤 패턴으로 개선할지 방향을 제시

단, 모든 패턴을 무조건 적용하는 것은 오히려 해롭다. 패턴은 문제가 있을 때 적용하는 도구이지, 코드를 복잡하게 만드는 의식이 아니다.

GoF 23 패턴 분류

생성(Creational) 5: 객체 생성 메커니즘
├── Factory Method
├── Abstract Factory
├── Builder
├── Singleton
└── Prototype

구조(Structural) 7: 객체 합성/구조화
├── Adapter
├── Bridge
├── Composite
├── Decorator
├── Facade
├── Flyweight
└── Proxy

행위(Behavioral) 11: 객체 간 상호작용
├── Chain of Responsibility
├── Command
├── Iterator
├── Mediator
├── Memento
├── Observer
├── State
├── Strategy
├── Template Method
├── Visitor
└── Interpreter

이 글에서는 실무에서 가장 자주 사용하는 15개 GoF 패턴 + 5개 현대 패턴을 TypeScript, Python, Go로 구현하며 배운다.


2. SOLID 원칙: 패턴의 기반

디자인 패턴을 이해하기 전에 SOLID 원칙을 먼저 살펴보자. 대부분의 패턴은 이 원칙들을 실현하기 위한 구체적인 방법이다.

2.1 SRP — 단일 책임 원칙

클래스는 변경의 이유가 하나뿐이어야 한다.

// Bad: 여러 책임이 혼재
class UserService {
  createUser(data: UserData): User { /* ... */ }
  sendWelcomeEmail(user: User): void { /* ... */ }
  generateReport(users: User[]): PDF { /* ... */ }
}

// Good: 책임 분리
class UserService {
  createUser(data: UserData): User { /* ... */ }
}

class EmailService {
  sendWelcomeEmail(user: User): void { /* ... */ }
}

class ReportService {
  generateReport(users: User[]): PDF { /* ... */ }
}

2.2 OCP — 개방/폐쇄 원칙

확장에는 열려 있고 수정에는 닫혀 있어야 한다. Strategy, Decorator, Observer 패턴이 이 원칙을 실현한다.

# Bad: 새 할인 유형마다 코드 수정 필요
def calculate_discount(order, discount_type):
    if discount_type == "percentage":
        return order.total * 0.1
    elif discount_type == "fixed":
        return 10.0
    # 새 할인 추가 시 여기를 수정해야 함

# Good: 새 할인 유형을 추가만 하면 됨
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 — 리스코프 치환 원칙

하위 타입은 상위 타입을 대체할 수 있어야 한다. 정사각형은 직사각형을 상속하면 안 되는 이유가 바로 이것이다.

2.4 ISP — 인터페이스 분리 원칙

클라이언트가 사용하지 않는 메서드에 의존하면 안 된다.

// Bad: 거대한 인터페이스
type Worker interface {
    Work()
    Eat()
    Sleep()
}

// Good: 분리된 인터페이스
type Workable interface {
    Work()
}

type Eatable interface {
    Eat()
}

// Go는 인터페이스 합성이 자연스럽다
type HumanWorker interface {
    Workable
    Eatable
}

2.5 DIP — 의존성 역전 원칙

고수준 모듈이 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다. Dependency Injection의 이론적 근거이다.


3. 생성 패턴 (Creational Patterns)

3.1 Factory Method 패턴

문제: 객체 생성 로직이 클라이언트 코드에 직접 박혀 있으면, 새로운 타입 추가 시 모든 생성 코드를 수정해야 한다.

해결: 객체 생성을 서브클래스에 위임한다.

// 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();
}

// 사용
const logger = createLogger("cloud");
logger.log("Application started");
# Python Factory Method
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()

# 사용
notifier = NotificationFactory.create(NotificationType.SLACK)
notifier.send("team-channel", "Deploy complete!")

사용 시점: 생성할 객체 타입을 런타임에 결정해야 할 때, 객체 생성 로직을 중앙 집중화하고 싶을 때.

사용하지 말아야 할 때: 객체 타입이 하나뿐이고 변경 가능성이 없을 때. 불필요한 추상화는 복잡성만 증가시킨다.

3.2 Abstract Factory 패턴

문제: 관련 객체군을 일관성 있게 생성해야 한다.

해결: 관련 객체들의 팩토리를 추상화한다.

// TypeScript Abstract Factory — UI 테마 시스템
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 구현
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 구현
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); }
}

// 사용 — 테마 전환이 한 줄로
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 패턴

문제: 생성자 매개변수가 많은 객체를 만들 때, 어떤 매개변수가 무엇을 의미하는지 알기 어렵다(Telescoping Constructor 문제).

해결: 단계적으로 객체를 구성하는 빌더를 제공한다.

// 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;
  }
}

// 사용: 가독성이 뛰어남
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 — dataclass + 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

# 사용
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 패턴

문제: 특정 클래스의 인스턴스가 오직 하나만 존재해야 한다.

해결: 생성을 제한하고 전역 접근점을 제공한다.

// TypeScript Singleton — 현대적 접근
class ConfigManager {
  private static instance: ConfigManager | null = null;
  private config: Map<string, unknown> = new Map();

  private constructor() {
    // private 생성자 — 외부에서 new 불가
  }

  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);
  }
}

// 사용
const config = ConfigManager.getInstance();
config.set("database.host", "localhost");
// Go Singleton — 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
}

주의: Singleton은 전역 상태를 만들기 때문에 테스트가 어려워진다. DI 컨테이너에서 "single scope"로 관리하는 것이 더 현대적인 접근이다.

3.5 Prototype 패턴

문제: 복잡한 객체를 처음부터 생성하는 비용이 크다.

해결: 기존 객체를 복제(clone)하여 새 객체를 만든다.

// 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),
    );
  }
}

// 프로토타입 레지스트리
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]])),
};

// 복제하여 커스터마이즈
const myWarrior = archetypes.warrior.clone();
myWarrior.name = "Aragorn";
myWarrior.skills.push("Charge");

4. 구조 패턴 (Structural Patterns)

4.1 Adapter 패턴

문제: 인터페이스가 맞지 않는 기존 클래스를 사용해야 한다.

해결: 중간에 변환 레이어(어댑터)를 둔다.

// TypeScript Adapter — 외부 결제 라이브러리 통합
// 우리 시스템의 결제 인터페이스
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;
}

// 외부 라이브러리 (변경 불가)
class StripeSDK {
  async createCharge(params: {
    amount_cents: number;
    currency: string;
  }): Promise<{ id: string; status: string }> {
    // Stripe API 호출
    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,
    };
  }
}

// 사용 — 우리 코드는 PaymentProcessor만 알면 됨
const processor: PaymentProcessor = new StripeAdapter();
const result = await processor.charge(29.99, "USD");

4.2 Decorator 패턴

문제: 객체에 동적으로 새로운 책임을 추가해야 하지만, 서브클래싱은 유연하지 않다.

해결: 래퍼(wrapper) 객체로 감싸서 기능을 추가한다.

# Python Decorator — 로깅, 캐싱, 재시도를 조합
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]:
    """함수 호출을 로깅"""
    @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):
    """실패 시 재시도"""
    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: 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

# 데코레이터 조합 — 바깥에서 안쪽 순서로 적용
@with_logging
@with_retry(max_retries=3, delay=0.5)
@with_cache(ttl_seconds=60)
def fetch_user_profile(user_id: int) -> dict:
    """외부 API에서 사용자 프로필 조회"""
    import requests
    response = requests.get(f"https://api.example.com/users/{user_id}")
    response.raise_for_status()
    return response.json()

4.3 Facade 패턴

문제: 복잡한 서브시스템을 사용하기 위해 너무 많은 클래스와 메서드를 알아야 한다.

해결: 단순화된 인터페이스를 제공하는 Facade를 만든다.

// TypeScript Facade — 주문 처리 시스템
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 — 복잡한 프로세스를 단순한 메서드로
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. 재고 확인
    if (!this.inventory.checkStock(order.productId)) {
      throw new Error("Out of stock");
    }

    // 2. 재고 예약
    this.inventory.reserveStock(order.productId, order.quantity);

    // 3. 결제 처리
    const shippingCost = this.shipping.calculateShipping(order.address);
    const txnId = this.payment.processPayment(
      order.userId,
      order.amount + shippingCost,
    );

    // 4. 배송 생성
    const orderId = `ORD-${Date.now()}`;
    const trackingId = this.shipping.createShipment(orderId, order.address);

    // 5. 알림 발송
    this.notification.sendOrderConfirmation(order.email, orderId);

    return { orderId, trackingId };
  }
}

// 사용 — 클라이언트는 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 패턴

문제: 객체에 대한 접근을 제어해야 한다(지연 로딩, 접근 제어, 로깅 등).

해결: 실제 객체를 감싸는 프록시를 제공한다.

// TypeScript Proxy — 지연 로딩 + 접근 제어
interface Database {
  query(sql: string): Promise<unknown[]>;
  execute(sql: string): Promise<void>;
}

class PostgresDatabase implements Database {
  private pool: unknown; // 실제 DB 풀

  constructor(connectionString: string) {
    console.log("Establishing DB connection pool...");
    // 비용이 큰 초기화
    this.pool = {}; // 실제로는 connection 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 연결을 아직 하지 않음 — 지연 로딩
  }

  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 패턴

문제: 트리 구조를 가진 객체들을 개별 객체와 동일하게 다루고 싶다.

해결: 단일 객체와 복합 객체가 같은 인터페이스를 구현한다.

# Python Composite — 파일 시스템
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)

# 사용
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 패턴

문제: 알고리즘을 런타임에 교체해야 한다. if-else 또는 switch 체인이 계속 늘어난다.

해결: 알고리즘을 캡슐화하여 교환 가능하게 만든다.

// TypeScript Strategy — 가격 계산 전략
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"; }
}

class SeasonalPricing implements PricingStrategy {
  constructor(private seasonMultiplier: number) {}
  calculatePrice(basePrice: number, quantity: number): number {
    return basePrice * quantity * this.seasonMultiplier;
  }
  getName() { return "Seasonal"; }
}

// 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,
    );
  }
}

// 사용
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 패턴

문제: 한 객체의 상태 변경을 다른 객체들이 자동으로 감지해야 한다.

해결: 발행-구독(Pub/Sub) 메커니즘을 구현한다.

// TypeScript Observer — 타입 안전한 이벤트 시스템
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);

    // unsubscribe 함수 반환
    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);
  }
}

// 사용
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 패턴

문제: 요청을 객체로 캡슐화하여 큐잉, 로깅, 되돌리기(undo)를 구현해야 한다.

해결: 각 작업을 Command 객체로 만든다.

# Python Command — 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()

# 사용
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 패턴

문제: 객체의 행동이 내부 상태에 따라 완전히 달라진다. 거대한 if-else/switch가 만들어진다.

해결: 각 상태를 별도 클래스로 분리한다.

// TypeScript State — 주문 상태 머신
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); }
}

// 사용
const order = new Order();
order.confirm();   // Pending -> Confirmed
order.ship();      // Confirmed -> Shipped
order.deliver();   // Shipped -> Delivered

5.5 Template Method 패턴

문제: 알고리즘의 뼈대는 동일하지만, 일부 단계만 다르다.

해결: 기본 알고리즘을 부모 클래스에 정의하고, 변하는 단계만 하위 클래스에서 오버라이드한다.

# Python Template Method — 데이터 처리 파이프라인
from abc import ABC, abstractmethod
from typing import Any
import json
import csv
from io import StringIO

class DataProcessor(ABC):
    """데이터 처리 파이프라인 — 템플릿 메서드"""

    def process(self, source: str) -> dict[str, Any]:
        """템플릿 메서드: 처리 흐름을 정의"""
        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]:
        """데이터 추출 — 하위 클래스가 구현"""
        pass

    def validate(self, data: list[dict]) -> list[dict]:
        """기본 검증 — 필요 시 오버라이드"""
        return [row for row in data if row]

    @abstractmethod
    def transform(self, data: list[dict]) -> list[dict]:
        """데이터 변환 — 하위 클래스가 구현"""
        pass

    def load(self, data: list[dict]) -> dict[str, Any]:
        """결과 반환 — 기본 구현"""
        return {"records": len(data), "data": data}

    def notify(self, result: dict[str, Any]) -> None:
        """처리 완료 알림 — 선택적 오버라이드"""
        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 패턴

데이터 접근 로직을 비즈니스 로직으로부터 분리한다.

// 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;
}

// 구현 — PostgreSQL
class PostgresUserRepository implements Repository<User, string> {
  constructor(private pool: any) {} // pg.Pool

  async findById(id: string): Promise<User | null> {
    const result = await this.pool.query(
      "SELECT * FROM users WHERE id = $1", [id]
    );
    return result.rows[0] || null;
  }

  async findAll(filter?: Partial<User>): Promise<User[]> {
    let query = "SELECT * FROM users";
    const conditions: string[] = [];
    const params: unknown[] = [];

    if (filter?.role) {
      conditions.push(`role = $${params.length + 1}`);
      params.push(filter.role);
    }
    if (filter?.email) {
      conditions.push(`email = $${params.length + 1}`);
      params.push(filter.email);
    }

    if (conditions.length > 0) {
      query += " WHERE " + conditions.join(" AND ");
    }

    const result = await this.pool.query(query, params);
    return result.rows;
  }

  async create(data: Omit<User, "id">): Promise<User> {
    const result = await this.pool.query(
      "INSERT INTO users (name, email, role, created_at) VALUES ($1, $2, $3, $4) RETURNING *",
      [data.name, data.email, data.role, data.createdAt]
    );
    return result.rows[0];
  }

  async update(id: string, data: Partial<User>): Promise<User> {
    const sets: string[] = [];
    const params: unknown[] = [];

    Object.entries(data).forEach(([key, value]) => {
      if (key !== "id" && value !== undefined) {
        sets.push(`${key} = $${params.length + 1}`);
        params.push(value);
      }
    });

    params.push(id);
    const result = await this.pool.query(
      `UPDATE users SET ${sets.join(", ")} WHERE id = $${params.length} RETURNING *`,
      params
    );
    return result.rows[0];
  }

  async delete(id: string): Promise<boolean> {
    const result = await this.pool.query(
      "DELETE FROM users WHERE id = $1", [id]
    );
    return result.rowCount > 0;
  }
}

// In-Memory 구현 — 테스트용
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)

읽기(Query)와 쓰기(Command) 모델을 분리한다.

// TypeScript CQRS
// Command — 쓰기
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 — 읽기 (최적화된 별도 모델)
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 [];
  }

  async getOrdersByStatus(status: string): Promise<OrderSummary[]> {
    return [];
  }
}

6.3 Circuit Breaker 패턴

외부 서비스 장애가 전체 시스템으로 전파되는 것을 방지한다.

// 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;
  }
}

// 사용
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 패턴

비즈니스 규칙을 재사용 가능한 객체로 캡슐화한다.

# 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)

# 사용 예시
@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

class CategorySpec(Specification[Product]):
    def __init__(self, category: str):
        self.category = category

    def is_satisfied_by(self, product: Product) -> bool:
        return product.category == self.category

# 스펙 조합
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 패턴

여러 리포지토리에 걸친 변경을 하나의 트랜잭션으로 묶는다.

# 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()

# 사용 (SQLAlchemy 스타일)
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)

피해야 할 나쁜 설계 패턴들이다.

7.1 God Object (갓 오브젝트)

모든 것을 아는 거대한 클래스. SRP를 완전히 위반한다.

// Bad: God Object
class Application {
  // 사용자 관리
  createUser() { /* ... */ }
  deleteUser() { /* ... */ }
  authenticateUser() { /* ... */ }

  // 주문 관리
  createOrder() { /* ... */ }
  processPayment() { /* ... */ }

  // 이메일
  sendEmail() { /* ... */ }
  renderTemplate() { /* ... */ }

  // 로깅
  logError() { /* ... */ }
  logInfo() { /* ... */ }

  // 캐싱
  getFromCache() { /* ... */ }
  setToCache() { /* ... */ }
}

// 이 클래스는 10가지 이상의 이유로 변경될 수 있다

해결: 책임별로 클래스를 분리하고, Facade로 통합 인터페이스를 제공한다.

7.2 Spaghetti Code (스파게티 코드)

흐름을 추적하기 어려운 뒤엉킨 코드. 콜백 지옥, 중첩 if-else, goto 같은 패턴이 원인이다.

7.3 Golden Hammer (황금 망치)

모든 문제에 하나의 도구(패턴)만 적용하는 것. "패턴을 알면 모든 곳에 패턴을 넣는다."

7.4 Premature Optimization (성급한 최적화)

아직 병목이 아닌 곳을 최적화하느라 코드를 복잡하게 만드는 것.

7.5 Copy-Paste Programming

코드를 복사하여 약간만 수정하는 것. Template Method나 Strategy로 해결할 수 있다.


8. 패턴 선택 가이드

문제 상황                              추천 패턴
──────────────────────────────────────────────────
객체 생성이 복잡하다                    Builder
여러 알고리즘 중 하나를 선택해야 한다    Strategy
상태에 따라 행동이 변한다               State
인터페이스가 맞지 않는 코드를 통합       Adapter
기능을 동적으로 추가해야 한다           Decorator
복잡한 시스템을 단순화해야 한다         Facade
이벤트 기반 통신이 필요하다             Observer
작업 되돌리기가 필요하다                Command
데이터 접근을 추상화해야 한다           Repository
외부 서비스 장애 격리가 필요하다        Circuit Breaker

9. 퀴즈

Q1. Factory Method와 Abstract Factory의 차이점은?

Factory Method는 하나의 제품을 만드는 메서드를 오버라이드하는 패턴이고, Abstract Factory는 관련된 제품군(family)을 일관성 있게 만드는 패턴이다. UI 테마 시스템처럼 버튼, 인풋, 모달을 한 세트로 만들어야 할 때 Abstract Factory가 적합하다.

Q2. Strategy 패턴과 State 패턴의 구조는 비슷한데, 차이점은?

둘 다 행동을 위임하지만 의도가 다르다. Strategy는 알고리즘을 외부에서 선택하여 교체하는 것이고, State는 내부 상태 전이에 의해 자동으로 행동이 바뀌는 것이다. Strategy의 전환은 클라이언트가, State의 전환은 상태 객체 자체가 주도한다.

Q3. Decorator 패턴과 Proxy 패턴의 차이는?

둘 다 래퍼 객체지만 목적이 다르다. Decorator는 기능을 **추가(장식)**하고, Proxy는 접근을 제어한다. Decorator는 여러 겹 중첩이 일반적이지만, Proxy는 보통 한 겹이다.

Q4. CQRS는 언제 사용해야 하는가?

읽기와 쓰기의 부하 패턴이 크게 다를 때 적합하다. 예를 들어 쓰기는 복잡한 비즈니스 로직이 필요하지만 읽기는 단순한 조회가 대부분인 경우, 각각을 독립적으로 최적화할 수 있다. 단, 복잡성이 크게 증가하므로 단순한 CRUD 앱에는 과하다.

Q5. Circuit Breaker의 세 가지 상태와 전이 조건은?

CLOSED(정상): 요청이 정상 통과. 실패 횟수가 임계값을 넘으면 OPEN으로 전이. OPEN(차단): 모든 요청을 즉시 실패 처리. 일정 시간(recovery timeout) 경과 후 HALF_OPEN으로 전이. HALF_OPEN(시험): 제한된 수의 요청을 허용. 성공하면 CLOSED로, 실패하면 다시 OPEN으로 전이.


10. 참고 자료

  1. Gamma, E. et al. — Design Patterns: Elements of Reusable Object-Oriented Software (GoF Book)
  2. Martin, R.C. — Clean Architecture: A Craftsman's Guide to Software Structure and Design
  3. Fowler, M. — Patterns of Enterprise Application Architecture
  4. Refactoring Guru — https://refactoring.guru/design-patterns
  5. Head First Design Patterns (2nd Edition, 2020)
  6. Martin, R.C. — SOLID Principles Explained
  7. Microsoft — Cloud Design Patterns — https://learn.microsoft.com/en-us/azure/architecture/patterns/
  8. Nygard, M. — Release It! Design and Deploy Production-Ready Software
  9. Evans, E. — Domain-Driven Design: Tackling Complexity in the Heart of Software
  10. Fowler, M. — https://martinfowler.com/articles/enterprisePatterns.html
  11. Go Design Patterns — https://github.com/tmrts/go-patterns
  12. TypeScript Design Patterns — https://github.com/torokmark/design_patterns_in_typescript
  13. Python Design Patterns — https://python-patterns.guide/

Design Patterns Modern Guide 2025: GoF Patterns + Modern Patterns in TypeScript/Python/Go

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

  1. Gamma, E. et al. — Design Patterns: Elements of Reusable Object-Oriented Software (GoF Book)
  2. Martin, R.C. — Clean Architecture: A Craftsman's Guide to Software Structure and Design
  3. Fowler, M. — Patterns of Enterprise Application Architecture
  4. Refactoring Guru — https://refactoring.guru/design-patterns
  5. Head First Design Patterns (2nd Edition, 2020)
  6. Martin, R.C. — SOLID Principles Explained
  7. Microsoft — Cloud Design Patterns — https://learn.microsoft.com/en-us/azure/architecture/patterns/
  8. Nygard, M. — Release It! Design and Deploy Production-Ready Software
  9. Evans, E. — Domain-Driven Design: Tackling Complexity in the Heart of Software
  10. Fowler, M. — https://martinfowler.com/articles/enterprisePatterns.html
  11. Go Design Patterns — https://github.com/tmrts/go-patterns
  12. TypeScript Design Patterns — https://github.com/torokmark/design_patterns_in_typescript
  13. Python Design Patterns — https://python-patterns.guide/