🚀AdvancedAdvanced Patterns

JavaScript Design Patterns

Apply classic and JavaScript-specific design patterns Observer, Strategy, Command, Decorator, and Module to write maintainable production code.

Overview

Design patterns are reusable solutions to recurring software design problems. While many patterns originated in the Gang of Four book for statically-typed OOP languages, JavaScript's dynamic nature, first-class functions, and prototypal inheritance let you implement them more concisely often without the boilerplate those languages require. Knowing the patterns gives you a vocabulary: when a colleague says "use the Strategy pattern here", you immediately understand the intent. The most JavaScript-relevant patterns fall into three groups. Creational patterns control how objects are made Singleton, Factory, and Builder all appear frequently in frameworks and libraries. Structural patterns define how objects are composed Decorator and Proxy let you add behaviour without subclassing. Behavioural patterns coordinate object communication Observer, Command, and Strategy are pervasive in event-driven UIs, undo/redo systems, and plugin architectures. JavaScript's module system provides a natural Singleton via module-level state. Closures enable privacy without classes. Higher-order functions make Strategy trivial. Understanding both the pattern and its idiomatic JavaScript expression is what distinguishes senior engineers. You will encounter these patterns in React (Observer in Redux, Command in action creators, Strategy in middleware), Node.js (Decorator in Express middleware), and browser APIs (Observer in MutationObserver, EventEmitter).

Key Points

  • •Observer / EventEmitter: objects subscribe to events; emitting an event notifies all subscribers without tight coupling
  • •Strategy: encapsulate interchangeable algorithms behind a common interface; swap them without changing the caller
  • •Command: wrap a request as an object with execute/undo enables queuing, logging, and undo/redo
  • •Decorator: wrap an object to add behaviour without modifying it or creating a subclass
  • •Module pattern: use closures (or ES modules) to create private state and expose a public API
  • •Factory: a function or method that creates objects, hiding construction details from the caller
  • •Singleton: ensure a class or module has only one instance ES modules provide this automatically
  • •Builder: construct complex objects step-by-step with a fluent API, returning the result at the end

Examples

Observer / EventEmitter from scratch

class EventEmitter {
  #listeners = new Map();

  on(event, fn) {
    if (!this.#listeners.has(event)) {
      this.#listeners.set(event, new Set());
    }
    this.#listeners.get(event).add(fn);
    return () => this.off(event, fn); // returns unsubscribe function
  }

  off(event, fn) {
    this.#listeners.get(event)?.delete(fn);
  }

  emit(event, ...args) {
    this.#listeners.get(event)?.forEach(fn => fn(...args));
  }

  once(event, fn) {
    const wrapper = (...args) => { fn(...args); this.off(event, wrapper); };
    return this.on(event, wrapper);
  }
}

const store = new EventEmitter();
const unsub = store.on('change', data => console.log('Changed:', data));
store.once('ready', () => console.log('Ready!'));

store.emit('ready');         // Ready!
store.emit('change', 42);    // Changed: 42
store.emit('ready');         // (no output once fired once)
unsub();
store.emit('change', 99);    // (no output unsubscribed)

Returning an unsubscribe function from on() is more ergonomic than requiring a matching off() call with the original function reference.

Strategy pattern pluggable sort algorithms

// Each strategy is just a function no class hierarchy needed
const strategies = {
  bubble(arr) {
    const a = [...arr];
    for (let i = 0; i < a.length; i++)
      for (let j = 0; j < a.length - i - 1; j++)
        if (a[j] > a[j + 1]) [a[j], a[j + 1]] = [a[j + 1], a[j]];
    return a;
  },
  quick(arr) {
    if (arr.length <= 1) return arr;
    const pivot = arr[Math.floor(arr.length / 2)];
    const left  = arr.filter(x => x < pivot);
    const mid   = arr.filter(x => x === pivot);
    const right = arr.filter(x => x > pivot);
    return [...strategies.quick(left), ...mid, ...strategies.quick(right)];
  },
};

class Sorter {
  constructor(strategy = strategies.quick) {
    this.strategy = strategy;
  }

  setStrategy(strategy) { this.strategy = strategy; }
  sort(data) { return this.strategy(data); }
}

const sorter = new Sorter();
console.log(sorter.sort([5, 1, 4, 2, 3]));  // [1, 2, 3, 4, 5]
sorter.setStrategy(strategies.bubble);
console.log(sorter.sort([5, 1, 4, 2, 3]));  // [1, 2, 3, 4, 5]

In JavaScript, strategies are typically plain functions no Strategy interface class needed.

Command pattern with undo stack

class TextEditor {
  #text = '';
  #history = [];
  #future  = [];

  execute(command) {
    command.execute(this);
    this.#history.push(command);
    this.#future = [];
  }

  undo() {
    const cmd = this.#history.pop();
    if (cmd) { cmd.undo(this); this.#future.push(cmd); }
  }

  redo() {
    const cmd = this.#future.pop();
    if (cmd) { cmd.execute(this); this.#history.push(cmd); }
  }

  get text() { return this.#text; }
  set text(v) { this.#text = v; }
}

const insertCommand = (chars, position) => ({
  execute(editor) {
    const t = editor.text;
    editor.text = t.slice(0, position) + chars + t.slice(position);
  },
  undo(editor) {
    const t = editor.text;
    editor.text = t.slice(0, position) + t.slice(position + chars.length);
  }
});

const editor = new TextEditor();
editor.execute(insertCommand('Hello', 0));
editor.execute(insertCommand(' World', 5));
console.log(editor.text); // Hello World
editor.undo();
console.log(editor.text); // Hello
editor.redo();
console.log(editor.text); // Hello World

Commands as plain objects (not classes) are idiomatic JavaScript the execute/undo interface is enforced by convention.

Common Mistakes

  • Implementing Singleton with a global variable instead of a module-level export module-level singletons are automatically lazy and work correctly with bundlers
  • Mutating strategy state inside the strategy function strategies should be pure and stateless so they can be swapped freely
  • Adding listeners in a tight loop without keeping the unsubscribe reference leads to memory leaks and duplicate notifications
  • Making Command objects too granular every character typed should be batched into a word-level command, not a per-keystroke command, for practical undo/redo

Note

Prefer composition over inheritance for applying patterns. JavaScript closures and higher-order functions often let you express a pattern in 5 lines that would require 5 classes in Java. Always ask: "Can I replace this class hierarchy with a function that takes a function?"

Exercises

Write JavaScript code to solve each exercise. Run it and check your output.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What problem does the Observer pattern primarily solve?