Design patterns are reusable solutions to common software design problems. This guide covers the most important patterns with practical examples.
Creational Patterns #
Create objects in a controlled manner.
Singleton Pattern #
Ensures only one instance exists.
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
Database.instance = this;
}
connect() {
if (!this.connection) {
this.connection = 'Connected to database';
console.log(this.connection);
}
return this.connection;
}
query(sql) {
return `Executing: ${sql}`;
}
}
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true
Modern approach with closures:
const Database = (function() {
let instance;
function createInstance() {
return {
connection: 'Connected',
query(sql) {
return `Executing: ${sql}`;
}
};
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true
Use cases:
- Database connections
- Configuration managers
- Logging systems
- Cache
Factory Pattern #
Creates objects without specifying exact class.
class Car {
constructor(type) {
this.type = type;
this.wheels = 4;
}
drive() {
return `Driving ${this.type}`;
}
}
class Motorcycle {
constructor(type) {
this.type = type;
this.wheels = 2;
}
drive() {
return `Riding ${this.type}`;
}
}
class VehicleFactory {
createVehicle(type) {
switch(type.toLowerCase()) {
case 'car':
return new Car('sedan');
case 'suv':
return new Car('SUV');
case 'motorcycle':
return new Motorcycle('sport bike');
default:
throw new Error('Unknown vehicle type');
}
}
}
const factory = new VehicleFactory();
const car = factory.createVehicle('car');
const bike = factory.createVehicle('motorcycle');
console.log(car.drive()); // Driving sedan
console.log(bike.drive()); // Riding sport bike
Use cases:
- Creating different types of objects based on input
- Plugin systems
- Document generators
Builder Pattern #
Construct complex objects step by step.
class QueryBuilder {
constructor() {
this.query = {
select: [],
from: '',
where: [],
orderBy: ''
};
}
select(...fields) {
this.query.select = fields;
return this;
}
from(table) {
this.query.from = table;
return this;
}
where(condition) {
this.query.where.push(condition);
return this;
}
orderBy(field) {
this.query.orderBy = field;
return this;
}
build() {
const { select, from, where, orderBy } = this.query;
let sql = `SELECT ${select.join(', ')} FROM ${from}`;
if (where.length > 0) {
sql += ` WHERE ${where.join(' AND ')}`;
}
if (orderBy) {
sql += ` ORDER BY ${orderBy}`;
}
return sql;
}
}
const query = new QueryBuilder()
.select('name', 'email')
.from('users')
.where('age > 18')
.where('active = true')
.orderBy('name')
.build();
console.log(query);
// SELECT name, email FROM users WHERE age > 18 AND active = true ORDER BY name
Use cases:
- Complex object construction
- Query builders
- Form builders
- Configuration objects
Prototype Pattern #
Clone objects instead of creating from scratch.
const carPrototype = {
wheels: 4,
engine: 'V6',
clone() {
return Object.create(this);
},
describe() {
return `Car with ${this.wheels} wheels and ${this.engine} engine`;
}
};
const car1 = carPrototype.clone();
car1.color = 'red';
const car2 = carPrototype.clone();
car2.color = 'blue';
car2.engine = 'V8';
console.log(car1.describe()); // Car with 4 wheels and V6 engine
console.log(car2.describe()); // Car with 4 wheels and V8 engine
Use cases:
- Object cloning
- Avoid expensive initialization
- Prototypal inheritance
Structural Patterns #
Compose objects into larger structures.
Adapter Pattern #
Makes incompatible interfaces work together.
// Old API
class OldCalculator {
operations(term1, term2, operation) {
switch(operation) {
case 'add': return term1 + term2;
case 'sub': return term1 - term2;
default: return 0;
}
}
}
// New API
class NewCalculator {
add(term1, term2) {
return term1 + term2;
}
subtract(term1, term2) {
return term1 - term2;
}
}
// Adapter
class CalculatorAdapter {
constructor() {
this.calculator = new NewCalculator();
}
operations(term1, term2, operation) {
switch(operation) {
case 'add':
return this.calculator.add(term1, term2);
case 'sub':
return this.calculator.subtract(term1, term2);
default:
return 0;
}
}
}
const adapter = new CalculatorAdapter();
console.log(adapter.operations(5, 3, 'add')); // 8
Use cases:
- Third-party library integration
- Legacy code integration
- API versioning
Decorator Pattern #
Add functionality to objects dynamically.
class Coffee {
cost() {
return 5;
}
description() {
return 'Coffee';
}
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 2;
}
description() {
return this.coffee.description() + ', Milk';
}
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 1;
}
description() {
return this.coffee.description() + ', Sugar';
}
}
let coffee = new Coffee();
console.log(coffee.description(), '-', coffee.cost()); // Coffee - 5
coffee = new MilkDecorator(coffee);
console.log(coffee.description(), '-', coffee.cost()); // Coffee, Milk - 7
coffee = new SugarDecorator(coffee);
console.log(coffee.description(), '-', coffee.cost()); // Coffee, Milk, Sugar - 8
Use cases:
- Adding features to objects
- Middleware systems
- Plugin systems
Facade Pattern #
Simplified interface to complex subsystem.
class CPU {
freeze() { console.log('CPU: Freezing'); }
jump(position) { console.log(`CPU: Jumping to ${position}`); }
execute() { console.log('CPU: Executing'); }
}
class Memory {
load(position, data) {
console.log(`Memory: Loading ${data} at ${position}`);
}
}
class HardDrive {
read(sector, size) {
console.log(`HardDrive: Reading ${size} bytes from sector ${sector}`);
return 'boot data';
}
}
// Facade
class ComputerFacade {
constructor() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
start() {
this.cpu.freeze();
const bootData = this.hardDrive.read(0, 1024);
this.memory.load(0, bootData);
this.cpu.jump(0);
this.cpu.execute();
}
}
const computer = new ComputerFacade();
computer.start();Use cases:
- Simplifying complex libraries
- API gateways
- Framework interfaces
Proxy Pattern #
Control access to another object.
class Image {
constructor(filename) {
this.filename = filename;
this.loadFromDisk();
}
loadFromDisk() {
console.log(`Loading ${this.filename}`);
}
display() {
console.log(`Displaying ${this.filename}`);
}
}
// Lazy loading proxy
class ImageProxy {
constructor(filename) {
this.filename = filename;
this.image = null;
}
display() {
if (!this.image) {
this.image = new Image(this.filename);
}
this.image.display();
}
}
const photo = new ImageProxy('photo.jpg');
// Image not loaded yet
photo.display(); // Loads and displays
photo.display(); // Just displays (already loaded)
Protection proxy:
class BankAccount {
constructor(balance) {
this.balance = balance;
}
withdraw(amount) {
this.balance -= amount;
return this.balance;
}
}
class ProtectedBankAccount {
constructor(balance, password) {
this.account = new BankAccount(balance);
this.password = password;
}
withdraw(amount, password) {
if (password !== this.password) {
throw new Error('Invalid password');
}
return this.account.withdraw(amount);
}
}
const account = new ProtectedBankAccount(1000, 'secret');
console.log(account.withdraw(100, 'secret')); // 900
// account.withdraw(100, 'wrong'); // Error
Use cases:
- Lazy loading
- Access control
- Caching
- Logging
Behavioral Patterns #
Object communication and responsibility.
Observer Pattern #
Subscribe to and receive notifications.
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} received:`, data);
}
}
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('Hello observers!');
// Observer 1 received: Hello observers!
// Observer 2 received: Hello observers!
Real-world example:
class NewsPublisher {
constructor() {
this.subscribers = [];
}
subscribe(subscriber) {
this.subscribers.push(subscriber);
}
publish(article) {
this.subscribers.forEach(sub => sub.receive(article));
}
}
class NewsSubscriber {
constructor(name) {
this.name = name;
}
receive(article) {
console.log(`${this.name} received: ${article.title}`);
}
}
const publisher = new NewsPublisher();
const subscriber1 = new NewsSubscriber('Alice');
const subscriber2 = new NewsSubscriber('Bob');
publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);
publisher.publish({ title: 'Breaking News!' });Use cases:
- Event systems
- Data binding
- Pub/sub systems
- Real-time updates
Strategy Pattern #
Switch between different algorithms at runtime.
class PaymentContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
pay(amount) {
return this.strategy.pay(amount);
}
}
class CreditCardStrategy {
constructor(cardNumber) {
this.cardNumber = cardNumber;
}
pay(amount) {
console.log(`Paid $${amount} with credit card ${this.cardNumber}`);
}
}
class PayPalStrategy {
constructor(email) {
this.email = email;
}
pay(amount) {
console.log(`Paid $${amount} via PayPal: ${this.email}`);
}
}
class BitcoinStrategy {
constructor(address) {
this.address = address;
}
pay(amount) {
console.log(`Paid $${amount} with Bitcoin to ${this.address}`);
}
}
const payment = new PaymentContext(new CreditCardStrategy('1234-5678'));
payment.pay(100);
payment.setStrategy(new PayPalStrategy('user@example.com'));
payment.pay(50);Use cases:
- Multiple algorithms for same task
- Sorting strategies
- Compression strategies
- Validation strategies
Command Pattern #
Encapsulate requests as objects.
class Light {
on() {
console.log('Light is ON');
}
off() {
console.log('Light is OFF');
}
}
class LightOnCommand {
constructor(light) {
this.light = light;
}
execute() {
this.light.on();
}
undo() {
this.light.off();
}
}
class LightOffCommand {
constructor(light) {
this.light = light;
}
execute() {
this.light.off();
}
undo() {
this.light.on();
}
}
class RemoteControl {
constructor() {
this.history = [];
}
execute(command) {
command.execute();
this.history.push(command);
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
}
}
}
const light = new Light();
const lightOn = new LightOnCommand(light);
const lightOff = new LightOffCommand(light);
const remote = new RemoteControl();
remote.execute(lightOn); // Light is ON
remote.execute(lightOff); // Light is OFF
remote.undo(); // Light is ON
Use cases:
- Undo/redo functionality
- Transaction systems
- Macro recording
- Job queues
Iterator Pattern #
Access elements sequentially.
class Iterator {
constructor(items) {
this.index = 0;
this.items = items;
}
hasNext() {
return this.index < this.items.length;
}
next() {
return this.items[this.index++];
}
reset() {
this.index = 0;
}
}
class Collection {
constructor() {
this.items = [];
}
add(item) {
this.items.push(item);
}
createIterator() {
return new Iterator(this.items);
}
}
const collection = new Collection();
collection.add('A');
collection.add('B');
collection.add('C');
const iterator = collection.createIterator();
while (iterator.hasNext()) {
console.log(iterator.next());
}
// A, B, C
Use cases:
- Traversing collections
- Custom iteration logic
- Tree/graph traversal
Template Method Pattern #
Define algorithm skeleton, let subclasses override steps.
class DataProcessor {
process() {
this.readData();
this.processData();
this.saveData();
}
readData() {
throw new Error('Must implement readData');
}
processData() {
throw new Error('Must implement processData');
}
saveData() {
console.log('Saving data...');
}
}
class CSVProcessor extends DataProcessor {
readData() {
console.log('Reading CSV file');
}
processData() {
console.log('Processing CSV data');
}
}
class JSONProcessor extends DataProcessor {
readData() {
console.log('Reading JSON file');
}
processData() {
console.log('Processing JSON data');
}
}
const csv = new CSVProcessor();
csv.process();
// Reading CSV file
// Processing CSV data
// Saving data...
const json = new JSONProcessor();
json.process();
// Reading JSON file
// Processing JSON data
// Saving data...
Use cases:
- Framework hooks
- Data processing pipelines
- Test frameworks
Chain of Responsibility #
Pass requests along chain of handlers.
class Handler {
setNext(handler) {
this.nextHandler = handler;
return handler;
}
handle(request) {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null;
}
}
class AuthHandler extends Handler {
handle(request) {
if (!request.authenticated) {
console.log('Authentication failed');
return null;
}
console.log('Authenticated');
return super.handle(request);
}
}
class ValidationHandler extends Handler {
handle(request) {
if (!request.valid) {
console.log('Validation failed');
return null;
}
console.log('Validated');
return super.handle(request);
}
}
class ProcessHandler extends Handler {
handle(request) {
console.log('Processing request');
return true;
}
}
const auth = new AuthHandler();
const validation = new ValidationHandler();
const process = new ProcessHandler();
auth.setNext(validation).setNext(process);
auth.handle({ authenticated: true, valid: true });
// Authenticated
// Validated
// Processing request
Use cases:
- Middleware systems
- Event bubbling
- Logging pipelines
- Request processing
State Pattern #
Alter behavior when state changes.
class OrderState {
constructor(order) {
this.order = order;
}
cancel() {
throw new Error('Cannot cancel');
}
ship() {
throw new Error('Cannot ship');
}
deliver() {
throw new Error('Cannot deliver');
}
}
class PendingState extends OrderState {
cancel() {
console.log('Order cancelled');
this.order.setState(new CancelledState(this.order));
}
ship() {
console.log('Order shipped');
this.order.setState(new ShippedState(this.order));
}
}
class ShippedState extends OrderState {
deliver() {
console.log('Order delivered');
this.order.setState(new DeliveredState(this.order));
}
}
class DeliveredState extends OrderState {
// Terminal state
}
class CancelledState extends OrderState {
// Terminal state
}
class Order {
constructor() {
this.state = new PendingState(this);
}
setState(state) {
this.state = state;
}
cancel() {
this.state.cancel();
}
ship() {
this.state.ship();
}
deliver() {
this.state.deliver();
}
}
const order = new Order();
order.ship(); // Order shipped
order.deliver(); // Order delivered
Use cases:
- State machines
- Order processing
- UI state management
- Game character states
Pattern Selection Guide #
Creating objects:
- Multiple object types → Factory
- One instance → Singleton
- Complex construction → Builder
- Clone objects → Prototype
Object structure:
- Incompatible interfaces → Adapter
- Add features → Decorator
- Simplify complex system → Facade
- Control access → Proxy
Object behavior:
- Notifications → Observer
- Swappable algorithms → Strategy
- Request as object → Command
- Sequential access → Iterator
- Algorithm skeleton → Template Method
- Pass to chain → Chain of Responsibility
- State-dependent behavior → State
Best Practices #
- Don’t overuse - Patterns add complexity
- Understand problem first - Pattern fits problem
- Keep it simple - Simplest solution wins
- Combine patterns - Patterns work together
- Focus on intent - Not implementation details
- Refactor to patterns - Don’t force them
- Document pattern usage - Help other developers
Design patterns provide proven solutions to common problems. Use them wisely to create maintainable, flexible code.