JavaScriptCommon Design Patterns

Design Patterns in JavaScript

Design patterns are reusable solutions to recurring problems. The classic Gang of Four catalogue was written for static OO languages and many of those patterns become one-liners in JavaScript — first-class functions and closures replace whole class hierarchies. Below is a tour of the patterns that do still earn their keep in JS, with idiomatic implementations.

Module pattern

Before import/export, an IIFE created a scope to hide state and expose only what you wanted. The pattern is still useful inside a single file for encapsulating a small subsystem.

JS
const Counter = (() => {
  let count = 0;
  return {
    inc()    { count++; },
    value()  { return count; },
    reset()  { count = 0; },
  };
})();

Counter.inc(); Counter.inc();
Counter.value();   // 2

count is reachable only via the returned methods — the IIFE closure is private state. In modern code, an ES module gives you the same encapsulation at file scope, so you usually only see this pattern inside a function.

Singleton

A single shared instance, lazily created. The simplest version in modern JS is an exported module — but a class with a static accessor is a useful pattern too.

JS
class Config {
  static #instance;
  data = {};

  static get instance() {
    if (!Config.#instance) Config.#instance = new Config();
    return Config.#instance;
  }
}

Config.instance === Config.instance;   // true
Use sparingly
Singletons hide a global. Hidden globals make testing harder. Often a regular object passed into the code that needs it is clearer.
Factory

A factory is just a function that returns a new object — no new required, no class needed. In JavaScript this is so natural that "factory" sometimes feels like a pretentious name for "a function that returns an object".

JS
function createUser({ name, role = "user" }) {
  return {
    name,
    role,
    isAdmin: role === "admin",
    greet:   () => "Hello, " + name,
  };
}

const u = createUser({ name: "Ada", role: "admin" });
u.greet();   // "Hello, Ada"

Factories shine when the right shape of object depends on the inputs — return one type for guests, another for admins, all from the same function.

Observer (a.k.a. pub/sub)

One object holds a list of subscribers and notifies them when something happens. This is the engine behind event systems, reactivity, and React's render loop.

JS
function createObservable() {
  const subscribers = new Set();
  return {
    subscribe(fn)   { subscribers.add(fn); return () => subscribers.delete(fn); },
    notify(payload) { for (const fn of subscribers) fn(payload); },
  };
}

const orders = createObservable();
const off = orders.subscribe(o => console.log("new order:", o.id));

orders.notify({ id: 1 });   // new order: 1
off();
orders.notify({ id: 2 });   // nothing — unsubscribed

The DOM EventTarget and Node's EventEmitter are this pattern with a richer API.

Strategy

Swap the algorithm at runtime by passing a function. No class hierarchy needed — JS already has callable values.

JS
function sort(items, strategy) {
  return items.slice().sort(strategy);
}

const byName = (a, b) => a.name.localeCompare(b.name);
const byAge  = (a, b) => a.age - b.age;

const people = [{ name: "Ada", age: 36 }, { name: "Lin", age: 24 }];

sort(people, byName);
sort(people, byAge);

Every map, filter, sort callback you've ever written is the strategy pattern in disguise.

Decorator

Wrap an object or function to add behaviour without changing the original. Higher-order functions are the bread-and-butter form.

JS
function withLogging(fn) {
  return function (...args) {
    console.log("call", fn.name, args);
    const result = fn.apply(this, args);
    console.log("ret", fn.name, result);
    return result;
  };
}

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);

loggedAdd(2, 3);   // logs call/ret, returns 5

React HOCs like withRouter and the class-decorator syntax (@log) are the same idea at different scales.

Command

Represent an action as an object so you can queue, undo or log it. Useful in editors and reducers.

JS
const history = [];
function dispatch(state, command) {
  const next = command.apply(state);
  history.push(command);
  return next;
}

const addItem = (item) => ({
  apply: state => ({ ...state, items: [...state.items, item] }),
  undo:  state => ({ ...state, items: state.items.filter(i => i !== item) }),
});

let state = dispatch({ items: [] }, addItem("apple"));
State (small)

Different methods for different states. In JS the lightest form is an object map of handlers.

JS
const traffic = {
  red:    { next: "green" },
  green:  { next: "yellow" },
  yellow: { next: "red" },
};

let state = "red";
function tick() { state = traffic[state].next; }
tick(); tick();   // green, yellow
What is missing — and why
  • Abstract Factory, Visitor, Bridge — heavy patterns from typed OO. In JS, plain functions cover the same ground in a fraction of the code.

  • Iterator — built into the language via Symbol.iterator and for..of.

  • Prototype — JavaScript objects are literally prototypes; the pattern is the language.

Pattern fluency, not pattern obsession
Knowing patterns helps you name and discuss code. Forcing a "Builder" or "Mediator" onto code that fits in fifteen lines is the surest way to make a small problem big. Reach for the smallest tool that solves it.