Skip to content
Published on

Design Patterns 2025 Practical Guide: Reinterpreting GoF 23 Patterns in Modern Languages (TypeScript/Python/Go)

Authors

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 lingua franca of software engineering 30 years later. But the world has changed.

How the modern development landscape has evolved:

  • Shift from OOP-centric to functional + reactive hybrid approaches
  • Widespread adoption of microservices, serverless, and event-driven architectures
  • Modern languages like TypeScript, Go, and Rust have changed how patterns are implemented
  • Frameworks embed patterns internally, reducing the need for explicit implementation

Why you still need to know patterns:

  1. Shared vocabulary in code reviews — "How about applying Strategy pattern here?" communicates instantly
  2. Understanding framework internals — React, Spring, Express are all combinations of patterns
  3. Essential interview knowledge — Frequently tested alongside system design at FAANG
  4. Architectural judgment — Knowing when to use and when NOT to use them

Knowing patterns and overusing patterns are different things. This guide covers when to use and when to avoid each pattern.


2. TOP 10 Most Used Patterns

Ranked by frequency in modern codebases:

RankPatternCategoryFrequencyCommon Use Cases
1StrategyBehavioralVery HighPayments, auth, sorting
2ObserverBehavioralVery HighEvent systems, React
3Factory MethodCreationalHighDI containers, ORMs
4BuilderCreationalHighQuery builders, configs
5DecoratorStructuralHighMiddleware, AOP
6AdapterStructuralHighAPI wrappers, legacy integration
7SingletonCreationalMediumConfig, logger, DB pools
8ProxyStructuralMediumCaching, logging, access control
9CommandBehavioralMediumUndo/Redo, CQRS
10StateBehavioralMediumFSM, UI state management

3. Creational Patterns

3.1 Singleton — Ensuring a Single Instance

Problem: Global state (config, logger, DB pool) loses consistency if multiple instances are created

Solution: Restrict a class to a single instance with a global access point

TypeScript — Modern Implementation:

class AppConfig {
  private static instance: AppConfig;
  private config: Map<string, string> = new Map();

  private constructor() {
    // Block external construction
  }

  static getInstance(): AppConfig {
    if (!AppConfig.instance) {
      AppConfig.instance = new AppConfig();
    }
    return AppConfig.instance;
  }

  get(key: string): string | undefined {
    return this.config.get(key);
  }

  set(key: string, value: string): void {
    this.config.set(key, value);
  }
}

// Usage
const config = AppConfig.getInstance();
config.set('API_URL', 'https://api.example.com');

Python — Module-level Singleton (Pythonic):

# config.py - In Python, the module itself is a singleton
class _AppConfig:
    def __init__(self):
        self._config: dict[str, str] = {}

    def get(self, key: str) -> str | None:
        return self._config.get(key)

    def set(self, key: str, value: str) -> None:
        self._config[key] = value

# Module-level instance = singleton
app_config = _AppConfig()

When NOT to use:

  • When it makes testing difficult (replace with dependency injection)
  • When using it for convenience without needing global state
  • In multithreaded environments without considering synchronization

3.2 Factory Method — Delegating Object Creation

Problem: Object creation logic tightly coupled to client code makes extension difficult

Solution: Delegate object creation to subclasses or factory functions

// Notification system factory
interface Notification {
  send(message: string): Promise<void>;
}

class EmailNotification implements Notification {
  async send(message: string) {
    console.log(`Email: ${message}`);
  }
}

class SlackNotification implements Notification {
  async send(message: string) {
    console.log(`Slack: ${message}`);
  }
}

class SMSNotification implements Notification {
  async send(message: string) {
    console.log(`SMS: ${message}`);
  }
}

// Factory function (modern approach)
type NotificationType = 'email' | 'slack' | 'sms';

function createNotification(type: NotificationType): Notification {
  const notificationMap: Record<NotificationType, () => Notification> = {
    email: () => new EmailNotification(),
    slack: () => new SlackNotification(),
    sms: () => new SMSNotification(),
  };

  const creator = notificationMap[type];
  if (!creator) throw new Error(`Unknown notification type: ${type}`);
  return creator();
}

// Usage
const notifier = createNotification('slack');
await notifier.send('Deployment complete!');

Problem: Groups of related objects (e.g., UI themes) need to be created consistently

// UI Theme Abstract Factory
interface Button { render(): string; }
interface Input { render(): string; }

interface UIFactory {
  createButton(): Button;
  createInput(): Input;
}

class DarkThemeFactory implements UIFactory {
  createButton() { return { render: () => '<button class="dark-btn">Click</button>' }; }
  createInput() { return { render: () => '<input class="dark-input" />' }; }
}

class LightThemeFactory implements UIFactory {
  createButton() { return { render: () => '<button class="light-btn">Click</button>' }; }
  createInput() { return { render: () => '<input class="light-input" />' }; }
}

// Usage — just swap the factory to change the entire theme
function buildUI(factory: UIFactory) {
  const button = factory.createButton();
  const input = factory.createInput();
  return { button: button.render(), input: input.render() };
}

3.4 Builder — Step-by-Step Construction of Complex Objects

Problem: "Telescoping constructor" with 10+ parameters

// Fluent Builder pattern
class QueryBuilder {
  private table = '';
  private conditions: string[] = [];
  private orderByField = '';
  private limitCount = 0;

  from(table: string): this {
    this.table = table;
    return this;
  }

  where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }

  orderBy(field: string): this {
    this.orderByField = field;
    return this;
  }

  limit(count: number): this {
    this.limitCount = count;
    return this;
  }

  build(): string {
    let query = `SELECT * FROM ${this.table}`;
    if (this.conditions.length > 0) {
      query += ` WHERE ${this.conditions.join(' AND ')}`;
    }
    if (this.orderByField) {
      query += ` ORDER BY ${this.orderByField}`;
    }
    if (this.limitCount > 0) {
      query += ` LIMIT ${this.limitCount}`;
    }
    return query;
  }
}

// Usage — maximum readability
const query = new QueryBuilder()
  .from('users')
  .where('age > 18')
  .where('status = "active"')
  .orderBy('created_at DESC')
  .limit(10)
  .build();

3.5 Prototype — Creation Through Cloning

Problem: Object creation is expensive, or you need to create variations based on existing objects

interface Cloneable<T> {
  clone(): T;
}

class GameCharacter implements Cloneable<GameCharacter> {
  constructor(
    public name: string,
    public health: number,
    public inventory: string[],
    public position: { x: number; y: number }
  ) {}

  clone(): GameCharacter {
    return new GameCharacter(
      this.name,
      this.health,
      [...this.inventory],  // deep copy
      { ...this.position }   // deep copy
    );
  }
}

// Prototype registry
const templates = {
  warrior: new GameCharacter('Warrior', 100, ['sword', 'shield'], { x: 0, y: 0 }),
  mage: new GameCharacter('Mage', 70, ['staff', 'potion'], { x: 0, y: 0 }),
};

const player = templates.warrior.clone();
player.name = 'Hero Kim';

4. Structural Patterns

4.1 Adapter — Bridging Incompatible Interfaces

Problem: External API or legacy system interfaces don't match your codebase

// Existing external library (cannot modify)
class LegacyPaymentGateway {
  processPaymentInCents(amountCents: number, curr: string): boolean {
    console.log(`Processing ${amountCents} cents in ${curr}`);
    return true;
  }
}

// Our desired interface
interface PaymentProcessor {
  pay(amount: number, currency: string): Promise<boolean>;
}

// Adapter
class PaymentAdapter implements PaymentProcessor {
  constructor(private legacy: LegacyPaymentGateway) {}

  async pay(amount: number, currency: string): Promise<boolean> {
    const cents = Math.round(amount * 100);
    return this.legacy.processPaymentInCents(cents, currency);
  }
}

// Usage — client is unaware of the legacy system
const processor: PaymentProcessor = new PaymentAdapter(
  new LegacyPaymentGateway()
);
await processor.pay(29.99, 'USD');

4.2 Decorator — Dynamic Feature Addition

Problem: You want to add features to existing objects but inheritance is inflexible

// Middleware-style Decorator
interface HttpHandler {
  handle(request: Request): Promise<Response>;
}

class BaseHandler implements HttpHandler {
  async handle(req: Request): Promise<Response> {
    return new Response('OK', { status: 200 });
  }
}

class LoggingDecorator implements HttpHandler {
  constructor(private inner: HttpHandler) {}

  async handle(req: Request): Promise<Response> {
    console.log(`[LOG] ${req.method} ${req.url}`);
    const start = Date.now();
    const response = await this.inner.handle(req);
    console.log(`[LOG] Completed in ${Date.now() - start}ms`);
    return response;
  }
}

class AuthDecorator implements HttpHandler {
  constructor(private inner: HttpHandler) {}

  async handle(req: Request): Promise<Response> {
    const token = req.headers.get('Authorization');
    if (!token) return new Response('Unauthorized', { status: 401 });
    return this.inner.handle(req);
  }
}

// Decorator chaining — order matters!
const handler = new LoggingDecorator(
  new AuthDecorator(
    new BaseHandler()
  )
);

Python decorators (language built-in):

import functools
import time

def log_execution(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"[LOG] {func.__name__} took {elapsed:.3f}s")
        return result
    return wrapper

def retry(max_attempts: int = 3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    print(f"Retry {attempt + 1}/{max_attempts}")
        return wrapper
    return decorator

@log_execution
@retry(max_attempts=3)
def fetch_data(url: str) -> dict:
    ...

4.3 Proxy — Access Control and Additional Logic

Problem: You need to control access to an object or add behavior (caching, logging, lazy init)

// Caching Proxy
interface DataService {
  fetchUser(id: string): Promise<User>;
}

class RealDataService implements DataService {
  async fetchUser(id: string): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
  }
}

class CachingProxy implements DataService {
  private cache = new Map<string, { data: User; expiry: number }>();

  constructor(
    private real: DataService,
    private ttlMs: number = 60000
  ) {}

  async fetchUser(id: string): Promise<User> {
    const cached = this.cache.get(id);
    if (cached && cached.expiry > Date.now()) {
      console.log(`Cache HIT for user ${id}`);
      return cached.data;
    }

    console.log(`Cache MISS for user ${id}`);
    const data = await this.real.fetchUser(id);
    this.cache.set(id, { data, expiry: Date.now() + this.ttlMs });
    return data;
  }
}

4.4 Facade — Simple Interface for Complex Subsystems

// Complex subsystems
class VideoEncoder { encode(file: string) { /* ... */ } }
class AudioEncoder { encode(file: string) { /* ... */ } }
class Uploader { upload(file: string, dest: string) { /* ... */ } }
class Notifier { notify(userId: string, msg: string) { /* ... */ } }

// Facade — simple interface
class MediaPublisher {
  private videoEncoder = new VideoEncoder();
  private audioEncoder = new AudioEncoder();
  private uploader = new Uploader();
  private notifier = new Notifier();

  async publish(videoFile: string, userId: string): Promise<string> {
    this.videoEncoder.encode(videoFile);
    this.audioEncoder.encode(videoFile);
    const url = this.uploader.upload(videoFile, 'cdn-bucket');
    this.notifier.notify(userId, 'Video upload complete!');
    return url;
  }
}

4.5 Composite — Uniform Treatment of Tree Structures

interface FileSystemNode {
  name: string;
  getSize(): number;
  print(indent?: string): void;
}

class File implements FileSystemNode {
  constructor(public name: string, private size: number) {}
  getSize(): number { return this.size; }
  print(indent = ''): void {
    console.log(`${indent}FILE ${this.name} (${this.size}B)`);
  }
}

class Directory implements FileSystemNode {
  private children: FileSystemNode[] = [];
  constructor(public name: string) {}
  add(node: FileSystemNode): void { this.children.push(node); }
  getSize(): number {
    return this.children.reduce((sum, child) => sum + child.getSize(), 0);
  }
  print(indent = ''): void {
    console.log(`${indent}DIR ${this.name} (${this.getSize()}B)`);
    this.children.forEach(child => child.print(indent + '  '));
  }
}

5. Behavioral Patterns

5.1 Strategy — Interchangeable Algorithm Encapsulation

Problem: Infinite if-else branching for payments, sorting, authentication

// Payment strategies
interface PaymentStrategy {
  pay(amount: number): Promise<PaymentResult>;
  validate(): boolean;
}

class CreditCardPayment implements PaymentStrategy {
  constructor(private cardNumber: string) {}
  validate() { return this.cardNumber.length === 16; }
  async pay(amount: number) {
    return { success: true, method: 'credit_card', amount };
  }
}

class PayPalPayment implements PaymentStrategy {
  constructor(private email: string) {}
  validate() { return this.email.includes('@'); }
  async pay(amount: number) {
    return { success: true, method: 'paypal', amount };
  }
}

class StripePayment implements PaymentStrategy {
  constructor(private token: string) {}
  validate() { return this.token.length > 0; }
  async pay(amount: number) {
    return { success: true, method: 'stripe', amount };
  }
}

// Context
class PaymentProcessor {
  constructor(private strategy: PaymentStrategy) {}

  setStrategy(strategy: PaymentStrategy) {
    this.strategy = strategy;
  }

  async checkout(amount: number) {
    if (!this.strategy.validate()) {
      throw new Error('Invalid payment method');
    }
    return this.strategy.pay(amount);
  }
}

5.2 Observer — Automatic State Change Propagation

Problem: Multiple objects need to react to state changes without tight coupling

// Type-safe event bus
type EventMap = {
  'user:login': { userId: string; timestamp: Date };
  'user:logout': { userId: string };
  'order:created': { orderId: string; total: number };
  'order:shipped': { orderId: string; trackingNumber: string };
};

class TypedEventBus {
  private listeners = new Map<string, Set<Function>>();

  on<K extends keyof EventMap>(
    event: K,
    callback: (data: EventMap[K]) => void
  ): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);
    return () => this.listeners.get(event)?.delete(callback);
  }

  emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
    this.listeners.get(event)?.forEach(cb => cb(data));
  }
}

// Usage
const bus = new TypedEventBus();
const unsubscribe = bus.on('order:created', (data) => {
  console.log(`New order: ${data.orderId}, amount: ${data.total}`);
});

bus.emit('order:created', { orderId: 'ORD-001', total: 50000 });
unsubscribe(); // Clean up

5.3 Command — Objectifying Requests

Problem: You need Undo/Redo, macros, or request queuing

interface Command {
  execute(): void;
  undo(): void;
}

class TextEditor {
  content = '';
  private history: Command[] = [];
  private undone: Command[] = [];

  executeCommand(command: Command) {
    command.execute();
    this.history.push(command);
    this.undone = [];
  }

  undo() {
    const command = this.history.pop();
    if (command) {
      command.undo();
      this.undone.push(command);
    }
  }

  redo() {
    const command = this.undone.pop();
    if (command) {
      command.execute();
      this.history.push(command);
    }
  }
}

class InsertTextCommand implements Command {
  constructor(
    private editor: TextEditor,
    private text: string,
    private position: number
  ) {}

  execute() {
    const before = this.editor.content.slice(0, this.position);
    const after = this.editor.content.slice(this.position);
    this.editor.content = before + this.text + after;
  }

  undo() {
    const before = this.editor.content.slice(0, this.position);
    const after = this.editor.content.slice(this.position + this.text.length);
    this.editor.content = before + after;
  }
}

5.4 State — Behavior Changes Based on State (FSM)

// Order state FSM
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() { throw new Error('Cannot ship before payment'); }
  deliver() { throw new Error('Cannot deliver before shipping'); }
  cancel(order: Order) { order.setState(new CancelledState()); }
}

class ConfirmedState implements OrderState {
  name = 'CONFIRMED';
  confirm() { throw new Error('Already confirmed'); }
  ship(order: Order) { order.setState(new ShippedState()); }
  deliver() { throw new Error('Cannot deliver before shipping'); }
  cancel(order: Order) { order.setState(new CancelledState()); }
}

class ShippedState implements OrderState {
  name = 'SHIPPED';
  confirm() { throw new Error('Already confirmed'); }
  ship() { throw new Error('Already shipping'); }
  deliver(order: Order) { order.setState(new DeliveredState()); }
  cancel() { throw new Error('Cannot cancel while shipping'); }
}

class DeliveredState implements OrderState {
  name = 'DELIVERED';
  confirm() { throw new Error('Order completed'); }
  ship() { throw new Error('Order completed'); }
  deliver() { throw new Error('Order completed'); }
  cancel() { throw new Error('Order completed'); }
}

class CancelledState implements OrderState {
  name = 'CANCELLED';
  confirm() { throw new Error('Order cancelled'); }
  ship() { throw new Error('Order cancelled'); }
  deliver() { throw new Error('Order cancelled'); }
  cancel() { throw new Error('Already cancelled'); }
}

class Order {
  private state: OrderState = new PendingState();
  setState(state: OrderState) { this.state = state; }
  getStateName() { return this.state.name; }
  confirm() { this.state.confirm(this); }
  ship() { this.state.ship(this); }
  deliver() { this.state.deliver(this); }
  cancel() { this.state.cancel(this); }
}

5.5 Chain of Responsibility — Chain Processing of Requests

// Express middleware-style
interface Middleware {
  handle(req: Request, next: () => Promise<Response>): Promise<Response>;
}

class CorsMiddleware implements Middleware {
  async handle(req: Request, next: () => Promise<Response>) {
    const response = await next();
    response.headers.set('Access-Control-Allow-Origin', '*');
    return response;
  }
}

class RateLimitMiddleware implements Middleware {
  private requests = new Map<string, number[]>();

  async handle(req: Request, next: () => Promise<Response>) {
    const ip = req.headers.get('x-forwarded-for') || 'unknown';
    const now = Date.now();
    const timestamps = (this.requests.get(ip) || []).filter(t => t > now - 60000);

    if (timestamps.length >= 100) {
      return new Response('Too Many Requests', { status: 429 });
    }
    timestamps.push(now);
    this.requests.set(ip, timestamps);
    return next();
  }
}

class MiddlewareChain {
  private middlewares: Middleware[] = [];

  use(middleware: Middleware): this {
    this.middlewares.push(middleware);
    return this;
  }

  async execute(req: Request, finalHandler: () => Promise<Response>): Promise<Response> {
    let index = 0;
    const next = async (): Promise<Response> => {
      if (index >= this.middlewares.length) return finalHandler();
      return this.middlewares[index++].handle(req, next);
    };
    return next();
  }
}

5.6 Template Method — Defining Algorithm Skeletons

abstract class DataProcessor {
  // Template method — algorithm skeleton
  async process(): Promise<void> {
    const raw = await this.extract();
    const transformed = this.transform(raw);
    await this.validate(transformed);
    await this.load(transformed);
  }

  abstract extract(): Promise<string[]>;
  abstract transform(data: string[]): Record<string, unknown>[];
  async validate(data: Record<string, unknown>[]): Promise<void> {
    if (data.length === 0) throw new Error('No data');
  }
  abstract load(data: Record<string, unknown>[]): Promise<void>;
}

class CSVProcessor extends DataProcessor {
  async extract() { return ['name,age', 'Kim,30', 'Lee,25']; }
  transform(data: string[]) {
    const headers = data[0].split(',');
    return data.slice(1).map(row => {
      const values = row.split(',');
      return Object.fromEntries(headers.map((h, i) => [h, values[i]]));
    });
  }
  async load(data: Record<string, unknown>[]) {
    console.log('Saving to DB:', data);
  }
}

6. Modern Alternatives — Functional Patterns

Many GoF patterns can be expressed more concisely with functional programming.

GoF PatternFunctional AlternativeDescription
StrategyHigher-order functionsPass functions as arguments
ObserverRxJS / SignalsReactive streams
CommandClosures + function stacksThe function itself IS the command
FactoryFactory functionsCreate without classes
Template MethodFunction compositionpipe / compose
DecoratorFunction wrappingHigher-order function wrapping
SingletonModule-scoped variablesES modules are singletons
// Strategy as functional
type SortStrategy<T> = (a: T, b: T) => number;

const byName: SortStrategy<User> = (a, b) => a.name.localeCompare(b.name);
const byAge: SortStrategy<User> = (a, b) => a.age - b.age;

// Swapping strategy = changing function argument
const sorted = users.sort(byAge);

// Decorator as functional
const withLogging = <T extends (...args: any[]) => any>(fn: T): T => {
  return ((...args: any[]) => {
    console.log(`Calling ${fn.name} with`, args);
    const result = fn(...args);
    console.log(`Result:`, result);
    return result;
  }) as T;
};

const add = (a: number, b: number) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(1, 2);

Dependency Injection — Modern alternative to Factory/Singleton:

// Manual DI (no framework)
interface Logger { log(msg: string): void; }
interface Database { query(sql: string): Promise<any[]>; }

class UserService {
  constructor(
    private logger: Logger,
    private db: Database
  ) {}

  async getUser(id: string) {
    this.logger.log(`Fetching user ${id}`);
    return this.db.query(`SELECT * FROM users WHERE id = '${id}'`);
  }
}

// Inject mocks in tests
const mockLogger: Logger = { log: jest.fn() };
const mockDb: Database = { query: jest.fn().mockResolvedValue([]) };
const service = new UserService(mockLogger, mockDb);

7. Design Patterns Inside Frameworks

React Patterns

PatternUsage in React
ObserveruseState, useEffect, state subscriptions
CompositeComponent tree (JSX)
StrategyRender props, custom hooks
HOC (Decorator)withAuth, withTheme
FactorycreateElement
ProxyReact.lazy (lazy loading)

Express/Koa Patterns

PatternUsage in Express
Chain of ResponsibilityMiddleware chain
Decoratorapp.use() middleware wrapping
StrategyRoute handlers
Adapterbody-parser, cors, etc.

Spring Patterns

PatternUsage in Spring
FactoryApplicationContext (Bean Factory)
ProxyAOP, @Transactional
Template MethodJdbcTemplate, RestTemplate
SingletonDefault bean scope
ObserverApplicationEvent

8. Anti-Patterns — What to Avoid

8.1 God Object

Cramming all logic into a single class.

// BAD - God Object
class AppManager {
  createUser() { /* ... */ }
  deleteUser() { /* ... */ }
  processPayment() { /* ... */ }
  sendEmail() { /* ... */ }
  log() { /* ... */ }
  getConfig() { /* ... */ }
  // ... 500+ lines
}

// GOOD - Single Responsibility Principle
class UserService { /* users only */ }
class PaymentService { /* payments only */ }
class EmailService { /* emails only */ }

8.2 Singleton Abuse

// BAD - untestable singleton dependency
class OrderService {
  processOrder(orderId: string) {
    const db = DatabaseConnection.getInstance(); // hardcoded dependency
    const logger = Logger.getInstance();         // cannot replace in tests
  }
}

// GOOD - dependency injection
class OrderService {
  constructor(
    private db: DatabaseConnection,
    private logger: Logger
  ) {}
}

8.3 Over-Engineering

// BAD - excessive abstraction (YAGNI violation)
interface IStringFormatter { format(s: string): string; }
class UpperCaseFormatter implements IStringFormatter {
  format(s: string) { return s.toUpperCase(); }
}

// GOOD - KISS principle
const toUpper = (s: string) => s.toUpperCase();

9. Interview TOP 10 Pattern Questions

Q1: What are the problems with Singleton and its alternatives?

Answer: Singleton causes testing difficulties due to global state, hidden dependencies, and multithreading synchronization issues. The alternative is Dependency Injection (DI). When a DI container manages lifecycle, you maintain Singleton benefits while ensuring testability.

Q2: Strategy vs State pattern differences?

Answer: Strategy is for algorithm replacement where the client selects the strategy. State is for behavior changes based on state where the state object determines transitions. Strategy suits independent algorithms; State suits state machines.

Q3: How to prevent memory leaks in Observer pattern?

Answer: Always implement unsubscribe, use WeakRef or WeakMap to allow garbage collection. In React, unsubscribe in useEffect cleanup functions.

Q4: Adapter vs Facade differences?

Answer: Adapter converts one interface to another (1:1 conversion). Facade provides a simple interface to a complex subsystem (N:1 simplification).

Q5: When should you NOT use design patterns?

Answer: When the problem is simple, when it creates excessive abstraction that teammates cannot understand, and when it violates YAGNI. Patterns should only be introduced when solving real problems.

Q6: Factory Method vs Abstract Factory?

Answer: Factory Method delegates single object creation to subclasses. Abstract Factory creates an entire family of related objects. Use Abstract Factory when multiple related objects need to change together, like UI themes.

Q7: Decorator vs Proxy differences?

Answer: Decorator is for adding features (can stack multiple layers). Proxy is for access control (caching, logging, lazy init). Decorators dynamically compose at runtime; Proxy is typically a single wrapper.

Q8: How to apply Command pattern in CQRS?

Answer: In CQRS, Command objectifies state change requests. Write operations are encapsulated as CreateOrderCommand, UpdateUserCommand, etc. Combined with event sourcing, it enables tracking all change history.

Q9: Template Method vs Strategy?

Answer: Template Method is inheritance-based — fixes the algorithm skeleton, overrides specific steps. Strategy is composition-based — replaces the entire algorithm. Modern development prefers Strategy (composition).

Q10: What patterns are commonly used in microservices?

Answer: Circuit Breaker (fault isolation), Saga (distributed transactions), API Gateway (Facade), Service Discovery (Observer variant), Sidecar (Proxy), Event Sourcing (Command). These are GoF patterns extended to architecture level.


10. Practice Quiz

Q1: What design patterns are used in this code?
const handler = new LoggingMiddleware(
  new AuthMiddleware(
    new RateLimitMiddleware(
      new ApiHandler()
    )
  )
);

Answer: Decorator + Chain of Responsibility

Each middleware wraps the inner handler (Decorator), and requests propagate through the chain (Chain of Responsibility). Express middleware is a classic example.

Q2: What pattern does React useState represent?

Answer: Observer Pattern

When you call the setter function returned by useState, React automatically re-renders components subscribed to that state. This is the core of the Observer pattern: automatic propagation of state changes.

Q3: Why prefer DI over Singleton?

Answer:

  1. Testability — DI allows easy mock injection
  2. Explicit dependencies — Dependencies are visible in constructor
  3. Flexible lifecycle — Request-scoped, session-scoped, etc.
  4. SOLID compliance — Supports Dependency Inversion Principle
Q4: When should you use / not use Builder?

Use when:

  • Constructor has 4+ parameters
  • Many optional parameters
  • Building immutable objects step-by-step
  • Same construction process for different representations

Do NOT use when:

  • 2-3 parameters or fewer (over-engineering)
  • Simple DTOs or data classes
  • Language has named parameters (Python kwargs, Kotlin named args)
Q5: Pick the right pattern for this scenario.

"EC2 instance behavior of start, stop, terminate must change based on state (running, stopped, terminated)."

Answer: State Pattern

Represent each state as a class, and let the current state object determine behavior. Instead of checking state with if-else, delegate behavior to state objects.


11. References

Books

  1. "Design Patterns: Elements of Reusable Object-Oriented Software" — GoF (1994)
  2. "Head First Design Patterns" — Freeman & Robson (2020, 2nd ed.)
  3. "Patterns of Enterprise Application Architecture" — Martin Fowler
  4. "Refactoring to Patterns" — Joshua Kerievsky
  5. "A Philosophy of Software Design" — John Ousterhout

Online Resources

  1. Refactoring.Guru — Design Patterns — Visual pattern catalog
  2. Source Making — Design Patterns
  3. TypeScript Design Patterns
  4. Python Design Patterns
  5. Go Design Patterns

Courses and Videos

  1. Christopher Okhravi — Design Patterns
  2. Derek Banas — Design Patterns Tutorial

Architecture Patterns

  1. Martin Fowler — Patterns of Enterprise Architecture
  2. Microsoft — Cloud Design Patterns
  3. Microservices Patterns — Chris Richardson
  4. Game Programming Patterns — Robert Nystrom (free web book)