Design Patterns

Common JavaScript patterns: Singleton, Factory, Observer, Module, and more

JavaScript Design Patterns

Design patterns are reusable solutions to common programming problems. They provide proven approaches to structure code, making it more maintainable, scalable, and easier to understand.

Pattern Categories

  • Creational — Object creation (Singleton, Factory)
  • Structural — Object composition (Decorator, Facade)
  • Behavioral — Object communication (Observer, Strategy)

Singleton Pattern

Ensures only one instance of a class exists throughout the application:

// ES6 Module Singleton (simplest approach)
// database.js
class Database {
  constructor() {
    this.connection = null;
  }
  
  connect(url) {
    if (!this.connection) {
      this.connection = `Connected to ${url}`;
    }
    return this.connection;
  }
}

export const database = new Database();

// Usage - same instance everywhere
import { database } from "./database.js";
database.connect("mongodb://localhost");

// Class-based Singleton
class Singleton {
  static instance = null;
  
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    Singleton.instance = this;
  }
  
  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true

Factory Pattern

Creates objects without exposing the instantiation logic:

// Simple Factory
class Car {
  constructor(type) {
    this.type = type;
  }
}

class Truck {
  constructor(type) {
    this.type = type;
  }
}

class VehicleFactory {
  createVehicle(type) {
    switch (type) {
      case "car":
        return new Car(type);
      case "truck":
        return new Truck(type);
      default:
        throw new Error(`Unknown vehicle type: ${type}`);
    }
  }
}

const factory = new VehicleFactory();
const car = factory.createVehicle("car");
const truck = factory.createVehicle("truck");

// Factory Function (more common in JS)
function createUser(type) {
  const baseUser = {
    createdAt: new Date(),
    isActive: true,
  };
  
  const types = {
    admin: { role: "admin", permissions: ["read", "write", "delete"] },
    editor: { role: "editor", permissions: ["read", "write"] },
    viewer: { role: "viewer", permissions: ["read"] },
  };
  
  return { ...baseUser, ...types[type] };
}

const admin = createUser("admin");
const viewer = createUser("viewer");

Observer Pattern (Pub/Sub)

Objects subscribe to events and get notified when they occur:

class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
    
    // Return unsubscribe function
    return () => this.off(event, listener);
  }
  
  off(event, listener) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter(l => l !== listener);
  }
  
  emit(event, data) {
    if (!this.events[event]) return;
    this.events[event].forEach(listener => listener(data));
  }
  
  once(event, listener) {
    const wrapper = (data) => {
      listener(data);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
  }
}

// Usage
const emitter = new EventEmitter();

const unsubscribe = emitter.on("userLoggedIn", (user) => {
  console.log(`Welcome, ${user.name}!`);
});

emitter.emit("userLoggedIn", { name: "Alice" });
// "Welcome, Alice!"

unsubscribe(); // Stop listening

Module Pattern

Encapsulates private state and exposes a public API:

// IIFE Module (pre-ES6)
const Counter = (function() {
  // Private
  let count = 0;
  
  // Public API
  return {
    increment() {
      return ++count;
    },
    decrement() {
      return --count;
    },
    getCount() {
      return count;
    }
  };
})();

Counter.increment(); // 1
Counter.getCount();  // 1
Counter.count;       // undefined (private)

// Revealing Module Pattern
const Calculator = (function() {
  let result = 0;
  
  function add(x) {
    result += x;
    return this;
  }
  
  function subtract(x) {
    result -= x;
    return this;
  }
  
  function getResult() {
    return result;
  }
  
  function reset() {
    result = 0;
    return this;
  }
  
  // Reveal public methods
  return { add, subtract, getResult, reset };
})();

Calculator.add(5).add(3).subtract(2).getResult(); // 6

Strategy Pattern

Define a family of algorithms and make them interchangeable:

// Payment strategies
const paymentStrategies = {
  creditCard: (amount) => {
    console.log(`Paid $${amount} with credit card`);
    return { success: true, method: "creditCard" };
  },
  
  paypal: (amount) => {
    console.log(`Paid $${amount} via PayPal`);
    return { success: true, method: "paypal" };
  },
  
  crypto: (amount) => {
    console.log(`Paid $${amount} in crypto`);
    return { success: true, method: "crypto" };
  }
};

class PaymentProcessor {
  constructor(strategy) {
    this.strategy = strategy;
  }
  
  setStrategy(strategy) {
    this.strategy = strategy;
  }
  
  pay(amount) {
    return this.strategy(amount);
  }
}

const processor = new PaymentProcessor(paymentStrategies.creditCard);
processor.pay(100);

processor.setStrategy(paymentStrategies.paypal);
processor.pay(50);

Decorator Pattern

Add behavior to objects dynamically:

// Function decorator
function withLogging(fn) {
  return function(...args) {
    console.log(`Calling ${fn.name} with:`, args);
    const result = fn.apply(this, args);
    console.log(`Result:`, result);
    return result;
  };
}

function add(a, b) {
  return a + b;
}

const loggedAdd = withLogging(add);
loggedAdd(2, 3);
// "Calling add with: [2, 3]"
// "Result: 5"

// Object decorator
function withTimestamp(obj) {
  return {
    ...obj,
    createdAt: new Date(),
    updatedAt: new Date()
  };
}

function withValidation(obj) {
  return {
    ...obj,
    validate() {
      return Object.values(this).every(v => v !== null);
    }
  };
}

// Compose decorators
const user = withValidation(withTimestamp({
  name: "Alice",
  email: "alice@example.com"
}));

console.log(user.createdAt);  // Date
console.log(user.validate()); // true

Facade Pattern

Provide a simplified interface to a complex subsystem:

// Complex subsystem
class CPU {
  freeze() { console.log("CPU frozen"); }
  jump(address) { console.log(`Jumping to ${address}`); }
  execute() { console.log("CPU executing"); }
}

class Memory {
  load(address, data) { console.log(`Loading ${data} at ${address}`); }
}

class HardDrive {
  read(sector, size) { return "boot data"; }
}

// Facade - simplified interface
class ComputerFacade {
  constructor() {
    this.cpu = new CPU();
    this.memory = new Memory();
    this.hd = new HardDrive();
  }
  
  start() {
    this.cpu.freeze();
    this.memory.load(0, this.hd.read(0, 1024));
    this.cpu.jump(0);
    this.cpu.execute();
    console.log("Computer started!");
  }
}

// Simple usage
const computer = new ComputerFacade();
computer.start();

💡 Key Takeaways

  • Singleton — Single instance (ES modules are natural singletons)
  • Factory — Centralized object creation
  • Observer — Pub/sub for event-driven communication
  • Module — Encapsulation with public API
  • Strategy — Swappable algorithms
  • Decorator — Add behavior without modifying original
  • Facade — Simplify complex interfaces