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
| Problem | Pattern | Key Benefit |
|---|---|---|
| Need one instance | Singleton | Controlled access |
| Create objects by type | Factory | Decoupled creation |
| Complex object setup | Builder | Step-by-step construction |
| Incompatible interfaces | Adapter | Interface compatibility |
| Add behavior dynamically | Decorator | Flexible extension |
| Control object access | Proxy | Lazy loading, caching |
| Simplify complex system | Facade | Simple interface |
| Notify on changes | Observer | Loose coupling |
| Algorithm varies | Strategy | Runtime flexibility |
| Encapsulate request | Command | Undo/redo support |
| State-dependent behavior | State | Clean state transitions |
Interview Tips
- Name the pattern — “I’ll use the Observer pattern here”
- Explain why — “Because we need to notify multiple services”
- Show the structure — Draw class diagrams
- Implement key parts — Write the core classes
- Discuss trade-offs — “Pattern X is simpler but less flexible than Y”
- Don’t over-pattern — Use patterns where they naturally fit
- 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
- SOLID Principles — Patterns embody SOLID
- OOP Concepts — Foundation for patterns
- UML Class Diagrams — Visualize patterns
- LLD Problems — Patterns in practice
- Abstraction — Interfaces and abstract classes