Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Design Patterns

What are Design Patterns?

Design patterns are reusable solutions to common software design problems. They provide a shared vocabulary and proven approaches to recurring design challenges.

graph TD
    PATTERNS["Design Patterns"] --> CREATIONAL["Creational<br/>Object creation"]
    PATTERNS --> STRUCTURAL["Structural<br/>Object composition"]
    PATTERNS --> BEHAVIORAL["Behavioral<br/>Object interaction"]
    
    CREATIONAL --> S1["Singleton"]
    CREATIONAL --> S2["Factory Method"]
    CREATIONAL --> S3["Builder"]
    
    STRUCTURAL --> T1["Adapter"]
    STRUCTURAL --> T2["Decorator"]
    STRUCTURAL --> T3["Facade"]
    STRUCTURAL --> T4["Proxy"]
    
    BEHAVIORAL --> B1["Observer"]
    BEHAVIORAL --> B2["Strategy"]
    BEHAVIORAL --> B3["Command"]
    BEHAVIORAL --> B4["State"]

Creational Patterns

1. Singleton

Problem: Need exactly one instance of a class (e.g., database connection, config manager).

Python Implementation

class DatabaseConnection:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialize()
        return cls._instance
    
    def _initialize(self):
        self.connection = "Connected to DB"

# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True

Thread-safe version (Python):

import threading

class DatabaseConnection:
    _instance = None
    _lock = threading.Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:  # Double-checked locking
                    cls._instance = super().__new__(cls)
                    cls._instance._initialize()
        return cls._instance

Java Implementation

// Eager initialization (thread-safe, simplest)
public class DatabaseConnection {
    private static final DatabaseConnection INSTANCE = new DatabaseConnection();
    private String connection;
    
    private DatabaseConnection() {  // Private constructor
        this.connection = "Connected to DB";
    }
    
    public static DatabaseConnection getInstance() {
        return INSTANCE;
    }
    
    public String getConnection() { return connection; }
}

// Lazy initialization with double-checked locking
public class DatabaseConnectionLazy {
    private static volatile DatabaseConnectionLazy instance;
    private String connection;
    
    private DatabaseConnectionLazy() {
        this.connection = "Connected to DB";
    }
    
    public static DatabaseConnectionLazy getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnectionLazy.class) {
                if (instance == null) {
                    instance = new DatabaseConnectionLazy();
                }
            }
        }
        return instance;
    }
}

// Enum singleton (best for Java — thread-safe, serialization-safe)
public enum DatabaseConnectionEnum {
    INSTANCE;
    
    private String connection = "Connected to DB";
    
    public String getConnection() { return connection; }
}

When to use: Configuration, connection pools, logging, registry When NOT to use: When you need multiple instances, makes testing hard


2. Factory Method

Problem: Create objects without specifying exact class.

Python Implementation

from abc import ABC, abstractmethod

class Notification(ABC):
    @abstractmethod
    def send(self, message: str):
        pass

class EmailNotification(Notification):
    def send(self, message: str):
        print(f"Email: {message}")

class SMSNotification(Notification):
    def send(self, message: str):
        print(f"SMS: {message}")

class PushNotification(Notification):
    def send(self, message: str):
        print(f"Push: {message}")

class NotificationFactory:
    @staticmethod
    def create(notification_type: str) -> Notification:
        if notification_type == "email":
            return EmailNotification()
        elif notification_type == "sms":
            return SMSNotification()
        elif notification_type == "push":
            return PushNotification()
        else:
            raise ValueError(f"Unknown type: {notification_type}")

# Usage
notification = NotificationFactory.create("email")
notification.send("Hello!")

Java Implementation

// Product interface
public interface Notification {
    void send(String message);
}

// Concrete products
public class EmailNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Email: " + message);
    }
}

public class SMSNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("SMS: " + message);
    }
}

public class PushNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Push: " + message);
    }
}

// Factory
public class NotificationFactory {
    public static Notification create(String type) {
        return switch (type.toLowerCase()) {
            case "email" -> new EmailNotification();
            case "sms" -> new SMSNotification();
            case "push" -> new PushNotification();
            default -> throw new IllegalArgumentException("Unknown type: " + type);
        };
    }
}

// Usage
Notification notification = NotificationFactory.create("email");
notification.send("Hello!");

When to use: Object creation logic is complex, need to decouple creation from usage Real-world: Java Calendar.getInstance(), Python datetime.strptime()


3. Builder

Problem: Construct complex objects step by step.

Python Implementation

class House:
    def __init__(self):
        self.walls = None
        self.roof = None
        self.garage = None
        self.pool = None
    
    def __str__(self):
        parts = []
        if self.walls: parts.append(f"{self.walls} walls")
        if self.roof: parts.append(f"{self.roof} roof")
        if self.garage: parts.append("garage")
        if self.pool: parts.append("pool")
        return f"House with {', '.join(parts)}"

class HouseBuilder:
    def __init__(self):
        self.house = House()
    
    def set_walls(self, material: str) -> 'HouseBuilder':
        self.house.walls = material
        return self
    
    def set_roof(self, material: str) -> 'HouseBuilder':
        self.house.roof = material
        return self
    
    def add_garage(self) -> 'HouseBuilder':
        self.house.garage = True
        return self
    
    def add_pool(self) -> 'HouseBuilder':
        self.house.pool = True
        return self
    
    def build(self) -> House:
        return self.house

# Usage (fluent interface)
house = (HouseBuilder()
    .set_walls("brick")
    .set_roof("tile")
    .add_garage()
    .add_pool()
    .build())
print(house)  # House with brick walls, tile roof, garage, pool

Java Implementation

public class House {
    private final String walls;
    private final String roof;
    private final boolean garage;
    private final boolean pool;
    
    private House(HouseBuilder builder) {
        this.walls = builder.walls;
        this.roof = builder.roof;
        this.garage = builder.garage;
        this.pool = builder.pool;
    }
    
    @Override
    public String toString() {
        return String.format("House[walls=%s, roof=%s, garage=%b, pool=%b]",
            walls, roof, garage, pool);
    }
    
    public static class HouseBuilder {
        private String walls;
        private String roof;
        private boolean garage;
        private boolean pool;
        
        public HouseBuilder setWalls(String walls) {
            this.walls = walls;
            return this;
        }
        
        public HouseBuilder setRoof(String roof) {
            this.roof = roof;
            return this;
        }
        
        public HouseBuilder addGarage() {
            this.garage = true;
            return this;
        }
        
        public HouseBuilder addPool() {
            this.pool = true;
            return this;
        }
        
        public House build() {
            return new House(this);
        }
    }
}

// Usage
House house = new House.HouseBuilder()
    .setWalls("brick")
    .setRoof("tile")
    .addGarage()
    .addPool()
    .build();

When to use: Many optional parameters, complex construction, immutable objects Real-world: StringBuilder, SQLQueryBuilder, HttpClient.Builder


Structural Patterns

4. Adapter

Problem: Make incompatible interfaces work together.

# Old payment system
class OldPaymentSystem:
    def make_payment(self, amount: float):
        print(f"Old system: paying ${amount}")

# New interface expected by our app
class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount: float, currency: str):
        pass

# Adapter: wraps old system to match new interface
class OldPaymentAdapter(PaymentProcessor):
    def __init__(self, old_system: OldPaymentSystem):
        self.old_system = old_system
    
    def process_payment(self, amount: float, currency: str):
        converted = self._convert_to_usd(amount, currency)
        self.old_system.make_payment(converted)
    
    def _convert_to_usd(self, amount: float, currency: str) -> float:
        rates = {"EUR": 1.1, "GBP": 1.3}
        return amount * rates.get(currency, 1.0)

# Usage
processor = OldPaymentAdapter(OldPaymentSystem())
processor.process_payment(100, "EUR")  # Works with new interface

When to use: Integrating legacy code, third-party libraries with different interfaces

5. Decorator

Problem: Add behavior to objects dynamically without modifying their class.

from abc import ABC, abstractmethod

class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float:
        pass
    
    @abstractmethod
    def description(self) -> str:
        pass

class SimpleCoffee(Coffee):
    def cost(self) -> float:
        return 2.0
    
    def description(self) -> str:
        return "Simple coffee"

class CoffeeDecorator(Coffee, ABC):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee

class MilkDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.5
    
    def description(self) -> str:
        return self._coffee.description() + ", milk"

class SugarDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.25
    
    def description(self) -> str:
        return self._coffee.description() + ", sugar"

# Usage - stack decorators
coffee = SimpleCoffee()
coffee = MilkDecorator(coffee)
coffee = SugarDecorator(coffee)
print(f"{coffee.description()}: ${coffee.cost()}")
# Simple coffee, milk, sugar: $2.75

When to use: Add responsibilities dynamically, avoid subclass explosion Real-world: Java I/O streams (BufferedInputStream(FileInputStream(...)))

6. Proxy

Problem: Control access to an object.

class Image(ABC):
    @abstractmethod
    def display(self):
        pass

class RealImage(Image):
    def __init__(self, filename: str):
        self.filename = filename
        self._load_from_disk()
    
    def _load_from_disk(self):
        print(f"Loading {self.filename} from disk...")
    
    def display(self):
        print(f"Displaying {self.filename}")

class ProxyImage(Image):
    def __init__(self, filename: str):
        self.filename = filename
        self._real_image = None
    
    def display(self):
        if self._real_image is None:
            self._real_image = RealImage(self.filename)  # Lazy loading
        self._real_image.display()

# Usage
image = ProxyImage("photo.jpg")  # Not loaded yet
image.display()  # Loads from disk, then displays
image.display()  # Already loaded, just displays

When to use: Lazy loading, access control, caching, logging

7. Facade

Problem: Provide a simplified interface to a complex subsystem.

class CPU:
    def freeze(self): print("CPU: Freezing")
    def execute(self): print("CPU: Executing")
    def unfreeze(self): print("CPU: Unfreezing")

class Memory:
    def load(self, address: int, data: str): print(f"Memory: Loading {data} at {address}")

class HardDrive:
    def read(self, sector: int) -> str: return f"Data from sector {sector}"

class ComputerFacade:
    def __init__(self):
        self.cpu = CPU()
        self.memory = Memory()
        self.hard_drive = HardDrive()
    
    def start(self):
        print("Computer starting...")
        self.cpu.freeze()
        data = self.hard_drive.read(0)
        self.memory.load(0, data)
        self.cpu.execute()
        self.cpu.unfreeze()
        print("Computer started!")

# Usage - simple interface hides complexity
computer = ComputerFacade()
computer.start()

When to use: Simplify complex subsystems, reduce dependencies


Behavioral Patterns

8. Observer

Problem: Notify multiple objects when state changes.

Python Implementation

from abc import ABC, abstractmethod
from typing import List

class Observer(ABC):
    @abstractmethod
    def update(self, event: str, data: dict):
        pass

class EventEmitter:
    def __init__(self):
        self._observers: dict[str, List[Observer]] = {}
    
    def subscribe(self, event: str, observer: Observer):
        if event not in self._observers:
            self._observers[event] = []
        self._observers[event].append(observer)
    
    def unsubscribe(self, event: str, observer: Observer):
        self._observers[event].remove(observer)
    
    def emit(self, event: str, data: dict = None):
        for observer in self._observers.get(event, []):
            observer.update(event, data or {})

class OrderService(EventEmitter):
    def create_order(self, order_id: str, user_id: str):
        self.emit("order_created", {"order_id": order_id, "user_id": user_id})

# Observers
class EmailNotifier(Observer):
    def update(self, event: str, data: dict):
        print(f"Email: Order {data['order_id']} created for user {data['user_id']}")

class InventoryService(Observer):
    def update(self, event: str, data: dict):
        print(f"Inventory: Reserving items for order {data['order_id']}")

# Usage
order_service = OrderService()
order_service.subscribe("order_created", EmailNotifier())
order_service.subscribe("order_created", InventoryService())
order_service.create_order("ORD-123", "USER-456")

Java Implementation

// Observer interface
public interface OrderObserver {
    void onOrderCreated(String orderId, String userId);
}

// Subject (observable)
public class OrderService {
    private final List<OrderObserver> observers = new ArrayList<>();
    
    public void addObserver(OrderObserver observer) {
        observers.add(observer);
    }
    
    public void removeObserver(OrderObserver observer) {
        observers.remove(observer);
    }
    
    public void createOrder(String orderId, String userId) {
        // Create order logic...
        System.out.println("Order created: " + orderId);
        
        // Notify all observers
        for (OrderObserver observer : observers) {
            observer.onOrderCreated(orderId, userId);
        }
    }
}

// Concrete observers
public class EmailNotifier implements OrderObserver {
    @Override
    public void onOrderCreated(String orderId, String userId) {
        System.out.println("Email: Order " + orderId + " for user " + userId);
    }
}

public class InventoryService implements OrderObserver {
    @Override
    public void onOrderCreated(String orderId, String userId) {
        System.out.println("Inventory: Reserving items for " + orderId);
    }
}

public class AnalyticsService implements OrderObserver {
    @Override
    public void onOrderCreated(String orderId, String userId) {
        System.out.println("Analytics: Tracking order " + orderId);
    }
}

// Usage
OrderService orderService = new OrderService();
orderService.addObserver(new EmailNotifier());
orderService.addObserver(new InventoryService());
orderService.addObserver(new AnalyticsService());
orderService.createOrder("ORD-123", "USER-456");

When to use: Event systems, UI updates, microservice communication, pub/sub


9. Strategy

Problem: Algorithm varies at runtime.

Python Implementation

from abc import ABC, abstractmethod

class SortingStrategy(ABC):
    @abstractmethod
    def sort(self, data: list) -> list:
        pass

class BubbleSort(SortingStrategy):
    def sort(self, data: list) -> list:
        arr = data.copy()
        n = len(arr)
        for i in range(n):
            for j in range(0, n-i-1):
                if arr[j] > arr[j+1]:
                    arr[j], arr[j+1] = arr[j+1], arr[j]
        return arr

class QuickSort(SortingStrategy):
    def sort(self, data: list) -> list:
        if len(data) <= 1:
            return data
        pivot = data[len(data) // 2]
        left = [x for x in data if x < pivot]
        middle = [x for x in data if x == pivot]
        right = [x for x in data if x > pivot]
        return self.sort(left) + middle + self.sort(right)

class Sorter:
    def __init__(self, strategy: SortingStrategy):
        self._strategy = strategy
    
    def set_strategy(self, strategy: SortingStrategy):
        self._strategy = strategy
    
    def sort(self, data: list) -> list:
        return self._strategy.sort(data)

# Usage
sorter = Sorter(BubbleSort())
print(sorter.sort([3, 1, 4, 1, 5]))

sorter.set_strategy(QuickSort())
print(sorter.sort([3, 1, 4, 1, 5]))

Java Implementation

// Strategy interface
public interface SortingStrategy {
    int[] sort(int[] data);
}

// Concrete strategies
public class BubbleSort implements SortingStrategy {
    @Override
    public int[] sort(int[] data) {
        int[] arr = data.clone();
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        return arr;
    }
}

public class QuickSort implements SortingStrategy {
    @Override
    public int[] sort(int[] data) {
        int[] arr = data.clone();
        quickSort(arr, 0, arr.length - 1);
        return arr;
    }
    
    private void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            int pivot = partition(arr, low, high);
            quickSort(arr, low, pivot - 1);
            quickSort(arr, pivot + 1, high);
        }
    }
    
    private int partition(int[] arr, int low, int high) {
        int pivot = arr[high];
        int i = low - 1;
        for (int j = low; j < high; j++) {
            if (arr[j] < pivot) {
                i++;
                int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;
            }
        }
        int temp = arr[i + 1]; arr[i + 1] = arr[high]; arr[high] = temp;
        return i + 1;
    }
}

// Context
public class Sorter {
    private SortingStrategy strategy;
    
    public Sorter(SortingStrategy strategy) {
        this.strategy = strategy;
    }
    
    public void setStrategy(SortingStrategy strategy) {
        this.strategy = strategy;
    }
    
    public int[] sort(int[] data) {
        return strategy.sort(data);
    }
}

// Usage
Sorter sorter = new Sorter(new BubbleSort());
int[] result = sorter.sort(new int[]{3, 1, 4, 1, 5});

sorter.setStrategy(new QuickSort());
result = sorter.sort(new int[]{3, 1, 4, 1, 5});

When to use: Multiple algorithms, runtime selection, A/B testing, payment processing


10. Command

Problem: Encapsulate a request as an object.

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass
    
    @abstractmethod
    def undo(self):
        pass

class TextEditor:
    def __init__(self):
        self.content = ""
    
    def insert(self, text: str, position: int):
        self.content = self.content[:position] + text + self.content[position:]
    
    def delete(self, position: int, length: int):
        self.content = self.content[:position] + self.content[position+length:]

class InsertCommand(Command):
    def __init__(self, editor: TextEditor, text: str, position: int):
        self.editor = editor
        self.text = text
        self.position = position
    
    def execute(self):
        self.editor.insert(self.text, self.position)
    
    def undo(self):
        self.editor.delete(self.position, len(self.text))

class DeleteCommand(Command):
    def __init__(self, editor: TextEditor, position: int, length: int):
        self.editor = editor
        self.position = position
        self.length = length
        self.deleted_text = ""
    
    def execute(self):
        self.deleted_text = self.editor.content[self.position:self.position+self.length]
        self.editor.delete(self.position, self.length)
    
    def undo(self):
        self.editor.insert(self.deleted_text, self.position)

class CommandHistory:
    def __init__(self):
        self._history: list[Command] = []
    
    def execute(self, command: Command):
        command.execute()
        self._history.append(command)
    
    def undo(self):
        if self._history:
            command = self._history.pop()
            command.undo()

# Usage
editor = TextEditor()
history = CommandHistory()

history.execute(InsertCommand(editor, "Hello", 0))
print(editor.content)  # "Hello"

history.execute(InsertCommand(editor, " World", 5))
print(editor.content)  # "Hello World"

history.undo()
print(editor.content)  # "Hello"

When to use: Undo/redo, queuing operations, logging, macro recording


11. State

Problem: Object behavior changes based on internal state.

from abc import ABC, abstractmethod

class VendingMachineState(ABC):
    @abstractmethod
    def insert_money(self, machine: 'VendingMachine', amount: float):
        pass
    
    @abstractmethod
    def select_item(self, machine: 'VendingMachine', item: str):
        pass

class IdleState(VendingMachineState):
    def insert_money(self, machine, amount):
        machine.balance += amount
        machine.set_state(HasMoneyState())
        print(f"Inserted ${amount}. Balance: ${machine.balance}")
    
    def select_item(self, machine, item):
        print("Insert money first!")

class HasMoneyState(VendingMachineState):
    def insert_money(self, machine, amount):
        machine.balance += amount
        print(f"Inserted ${amount}. Balance: ${machine.balance}")
    
    def select_item(self, machine, item):
        if item in machine.items and machine.items[item] > 0:
            if machine.balance >= machine.prices[item]:
                machine.selected_item = item
                machine.items[item] -= 1
                change = machine.balance - machine.prices[item]
                print(f"Dispensing {item}. Change: ${change}")
                machine.balance = 0
                machine.selected_item = None
                machine.set_state(IdleState())
            else:
                print(f"Insufficient funds. Need ${machine.prices[item]}")
        else:
            print(f"Item {item} not available")

class VendingMachine:
    def __init__(self):
        self.items = {"coke": 5, "pepsi": 3, "water": 10}
        self.prices = {"coke": 1.5, "pepsi": 1.5, "water": 1.0}
        self.balance = 0.0
        self.selected_item = None
        self._state = IdleState()
    
    def set_state(self, state: VendingMachineState):
        self._state = state
    
    def insert_money(self, amount: float):
        self._state.insert_money(self, amount)
    
    def select_item(self, item: str):
        self._state.select_item(self, item)

# Usage
vm = VendingMachine()
vm.insert_money(2.0)
vm.select_item("coke")  # Dispensing coke. Change: $0.5

When to use: State machines, objects with distinct behaviors per state


Pattern Selection Guide

ProblemPatternKey Benefit
Need one instanceSingletonControlled access
Create objects by typeFactoryDecoupled creation
Complex object setupBuilderStep-by-step construction
Incompatible interfacesAdapterInterface compatibility
Add behavior dynamicallyDecoratorFlexible extension
Control object accessProxyLazy loading, caching
Simplify complex systemFacadeSimple interface
Notify on changesObserverLoose coupling
Algorithm variesStrategyRuntime flexibility
Encapsulate requestCommandUndo/redo support
State-dependent behaviorStateClean state transitions

Interview Tips

  1. Name the pattern — “I’ll use the Observer pattern here”
  2. Explain why — “Because we need to notify multiple services”
  3. Show the structure — Draw class diagrams
  4. Implement key parts — Write the core classes
  5. Discuss trade-offs — “Pattern X is simpler but less flexible than Y”
  6. Don’t over-pattern — Use patterns where they naturally fit
  7. Know the difference — Factory vs Builder, Strategy vs State, Proxy vs Decorator

Common Mistakes

  • ❌ Using Singleton everywhere (makes testing hard, global state)
  • ❌ Confusing Factory Method with Abstract Factory
  • ❌ Using Observer when direct method calls would suffice (over-engineering)
  • ❌ Confusing Strategy (algorithm selection) with State (behavior per state)
  • ❌ Not considering thread safety in Singleton implementations
  • ❌ Forcing patterns where simple code would work

Cross-References