Skip to content

Design Patterns (GoF) in JavaScript

The Gang of Four patterns apply differently in JavaScript than in class-based languages. First-class functions, closures, and dynamic typing simplify many patterns to plain functions or objects. In functional programming, all GoF patterns reduce to function composition: G(f(x)). ~20% of patterns cover ~80% of real-world use cases.

Creational Patterns

Factory Method

Replace direct new calls with method invocations for polymorphic instantiation:

// JavaScript simplified - no abstract classes needed
const creators = {
  electronics: (data) => ({ type: 'electronics', ...data }),
  clothing: (data) => ({ type: 'clothing', ...data }),
};
const create = (type, data) => creators[type](data);

// Factorify - factory from class
const factorify = (Entity) => (...args) => new Entity(...args);
const createUser = factorify(User);
const user = createUser('John', 25); // no `new` needed

Object Pool / AsyncPool

class AsyncPool {
  constructor(factory, size) {
    this.instances = Array.from({ length: size }, () => factory());
    this.free = new Array(size).fill(true);
    this.queue = [];
  }

  async getInstance() {
    for (let i = 0; i < this.instances.length; i++) {
      if (this.free[i]) { this.free[i] = false; return this.instances[i]; }
    }
    return new Promise((resolve) => this.queue.push(resolve));
  }

  release(instance) {
    const i = this.instances.indexOf(instance);
    if (this.queue.length > 0) this.queue.shift()(instance);
    else this.free[i] = true;
  }
}

Design: factory passed to constructor (not pre-made instances) so pool can grow on demand.

Flyweight (Timer Optimization)

Problem: 2 million setInterval calls is extremely expensive.

class Interval {
  static cache = new Map();
  constructor(duration, callback) {
    const cached = Interval.cache.get(duration);
    if (cached) { cached.callbacks.push(callback); return Object.setPrototypeOf(this, cached); }
    this.callbacks = [callback];
    this.timer = setInterval(() => { for (const cb of this.callbacks) cb(); }, duration);
    Interval.cache.set(duration, this);
  }
}

Result: 2 million instances but only 2 real setInterval calls (one per unique duration). Trade-off: polymorphic V8 dispatch (2 extra CMP per new).

Structural Patterns

Adapter vs Facade vs Proxy

Pattern Wraps Interface Purpose
Adapter ONE abstraction Changes interface Convert contracts (promisify, callbackify)
Facade Entire SUBSYSTEM Simplifies interface Hide complex internals
Proxy ONE abstraction Same interface Add behavior (caching, logging, access)

Adapter is extremely common - async contract conversion is everywhere in Node.js.

Observer / Observable

class Observable {
  #observers = [];
  subscribe(observer) { this.#observers.push(observer); }
  notify(data) { for (const o of this.#observers) o.update(data); }
  complete() { for (const o of this.#observers) o.complete?.(); }
}

JavaScript simplification: use plain callback functions instead of abstract Observer classes. Signals (modern frameworks) are also derived from Observable.

Behavioral Patterns

Strategy

const renderers = {
  console: (data) => console.table(data),
  web: (data) => `<table>${data.map(r => `<tr><td>${r.name}</td></tr>`).join('')}</table>`,
  markdown: (data) => data.map(r => `| ${r.name} |`).join('\n'),
};
const createContext = (type) => ({ process: (data) => renderers[type](data) });

In JS, a strategy can be: a function, a class, an object, or a module.

State

const states = {
  idle: { process: () => { /* idle behavior */ }, next: 'processing' },
  processing: { process: () => { /* work */ }, next: 'complete' },
  complete: { process: () => { /* noop */ }, next: 'idle' },
};

Chain of Responsibility

Middleware in Express/Fastify is a practical implementation. Each handler decides whether to process or pass to next().

Command (with Actor Model)

In the Actor model, messages are commands; the actor's message queue is a command queue. Combined with undo capability, enables transaction-like behavior.

FP Equivalents

GoF Pattern FP Equivalent
Strategy Passing a function as argument
Observer Callback composition
Decorator Function wrapping
Chain of Responsibility Function pipeline

Gotchas

  • Facades tend to grow into God Objects over time - discipline needed to keep the interface minimal
  • Adapter can have multiple inputs/outputs (USB-C to USB-A + Lightning is still an adapter)
  • Pareto Principle: ~20% of patterns cover ~80% of use cases - focus on patterns you'll actually use
  • All paradigms are highly variable internally - there's no single "OOP way" or "FP way"

See Also

  • [[solid-and-grasp]] - principles behind pattern selection
  • [[async-patterns]] - promisify/callbackify as Adapter pattern
  • [[dependency-injection]] - DI vs module system patterns
  • [[closures-and-scope]] - closures as the foundation for many patterns