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.
const Counter = (() => {
let count = 0;
return {
inc() { count++; },
value() { return count; },
reset() { count = 0; },
};
})();
Counter.inc(); Counter.inc();
Counter.value(); // 2count 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.
class Config {
static #instance;
data = {};
static get instance() {
if (!Config.#instance) Config.#instance = new Config();
return Config.#instance;
}
}
Config.instance === Config.instance; // trueFactory
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".
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.
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 — unsubscribedThe 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.
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.
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 5React 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.
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.
const traffic = {
red: { next: "green" },
green: { next: "yellow" },
yellow: { next: "red" },
};
let state = "red";
function tick() { state = traffic[state].next; }
tick(); tick(); // green, yellowWhat 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.iteratorandfor..of.Prototype — JavaScript objects are literally prototypes; the pattern is the language.