Design patterns are reusable solutions to common software design problems. JavaScript’s flexibility supports many classic patterns.

Module Pattern

Encapsulate private state with a public API:

  const Counter = (function() {
    let count = 0;

    return {
        increment() { return ++count; },
        decrement() { return --count; },
        getCount() { return count; }
    };
})();

Counter.increment(); // 1
Counter.getCount();  // 1
// count is not accessible from outside
  

ES modules provide native encapsulation:

  // counter.js
let count = 0;
export function increment() { return ++count; }
export function getCount() { return count; }
  

Singleton Pattern

Ensure only one instance exists:

  class Database {
    static #instance = null;

    constructor() {
        if (Database.#instance) {
            return Database.#instance;
        }
        this.connection = 'connected';
        Database.#instance = this;
    }
}

const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true
  

Factory Pattern

Create objects without specifying exact class:

  function createUser(type, name) {
    const users = {
        admin: () => ({ name, role: 'admin', permissions: ['read', 'write', 'delete'] }),
        guest: () => ({ name, role: 'guest', permissions: ['read'] })
    };
    return users[type]?.() ?? users.guest();
}

console.log(createUser('admin', 'Alice'));
  

Observer Pattern

Subscribe to and notify on state changes:

  class EventEmitter {
    constructor() {
        this.listeners = {};
    }

    on(event, callback) {
        (this.listeners[event] ??= []).push(callback);
    }

    emit(event, data) {
        (this.listeners[event] || []).forEach(cb => cb(data));
    }

    off(event, callback) {
        this.listeners[event] = (this.listeners[event] || [])
            .filter(cb => cb !== callback);
    }
}

const emitter = new EventEmitter();
emitter.on('data', (d) => console.log('Received:', d));
emitter.emit('data', { id: 1 }); // Received: { id: 1 }
  

Strategy Pattern

Swap algorithms at runtime:

  const strategies = {
    creditCard: (amount) => `Paid $${amount} with credit card`,
    paypal: (amount) => `Paid $${amount} with PayPal`,
    crypto: (amount) => `Paid $${amount} with crypto`
};

function checkout(method, amount) {
    return strategies[method]?.(amount) ?? 'Unknown payment method';
}

console.log(checkout('paypal', 99));
  

Decorator Pattern

Add behavior without modifying original object:

  function withLogging(fn) {
    return function(...args) {
        console.log(`Calling with`, args);
        const result = fn(...args);
        console.log(`Result:`, result);
        return result;
    };
}

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

const loggedAdd = withLogging(add);
loggedAdd(2, 3); // logs args and result
  

MVC / MVVM (Conceptual)

Common in front-end frameworks:

  • Model — data and business logic
  • View — UI presentation
  • Controller/ViewModel — connects model and view

React approximates this with components + state; Angular uses full MVC/MVVM.

When to Use Patterns

Pattern Use when
Module Hide implementation details
Singleton One shared resource (config, connection pool)
Factory Object type depends on input
Observer Event-driven updates (UI, pub/sub)
Strategy Multiple interchangeable algorithms
Decorator Extend behavior transparently

Use patterns to solve real problems — avoid over-engineering simple code.