Design Patterns - Complete Guide

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 #

  1. Don’t overuse - Patterns add complexity
  2. Understand problem first - Pattern fits problem
  3. Keep it simple - Simplest solution wins
  4. Combine patterns - Patterns work together
  5. Focus on intent - Not implementation details
  6. Refactor to patterns - Don’t force them
  7. Document pattern usage - Help other developers

Design patterns provide proven solutions to common problems. Use them wisely to create maintainable, flexible code.