JavaScript Design Patterns
JavaScript Design Patterns
Design patterns are proven solutions to recurring problems. In JavaScript, a small set of patterns comes up constantly in frameworks, libraries, and production codebases. Recognizing them makes unfamiliar code immediately understandable.
Observer / Pub-Sub
Components subscribe to events and get notified when they fire — without tight coupling:
class EventEmitter {
#listeners = new Map();
on(event, listener) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(listener);
return () => this.off(event, listener); // return unsubscribe function
}
off(event, listener) {
this.#listeners.get(event)?.delete(listener);
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach(listener => listener(...args));
}
once(event, listener) {
const wrapper = (...args) => {
listener(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
}
// Usage
const store = new EventEmitter();
const unsubscribe = store.on("userLoggedIn", (user) => {
updateNavbar(user);
loadDashboard(user.id);
});
store.emit("userLoggedIn", { id: 42, name: "Alice" });
// Later: clean up
unsubscribe();
Singleton
Ensure only one instance exists — used for caches, connection pools, app state:
class AppConfig {
static #instance = null;
#settings = {};
constructor() {
if (AppConfig.#instance) {
return AppConfig.#instance; // return existing instance
}
AppConfig.#instance = this;
this.#settings = { theme: "light", language: "en" };
}
static getInstance() {
if (!AppConfig.#instance) {
new AppConfig();
}
return AppConfig.#instance;
}
get(key) { return this.#settings[key]; }
set(key, value) { this.#settings[key] = value; }
}
const config1 = AppConfig.getInstance();
const config2 = AppConfig.getInstance();
config1 === config2; // true — same object
// Module-level singleton (simpler, common in real code)
export const config = {
theme: "light",
language: "en"
};
Factory Pattern
Create objects without specifying the exact class:
// Simple factory function
function createUser(type, data) {
const base = {
...data,
createdAt: new Date(),
isActive: true
};
switch (type) {
case "admin":
return { ...base, permissions: ["read", "write", "delete"], dashboard: "admin" };
case "editor":
return { ...base, permissions: ["read", "write"], dashboard: "editor" };
case "viewer":
return { ...base, permissions: ["read"], dashboard: "viewer" };
default:
throw new Error(`Unknown user type: ${type}`);
}
}
const admin = createUser("admin", { name: "Alice", email: "alice@example.com" });
Strategy Pattern
Define a family of algorithms and switch between them at runtime:
const sortStrategies = {
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 [...this.quick(left), ...mid, ...this.quick(right)];
},
builtin(arr) {
return [...arr].sort((a, b) => a - b);
}
};
function sort(arr, strategy = "builtin") {
return sortStrategies[strategy](arr);
}
// Real-world: payment strategies
const paymentProcessors = {
stripe: async (amount, card) => stripeAPI.charge(amount, card),
paypal: async (amount, email) => paypalAPI.pay(amount, email),
crypto: async (amount, wallet) => cryptoAPI.transfer(amount, wallet)
};
async function processPayment(method, ...args) {
const processor = paymentProcessors[method];
if (!processor) throw new Error(`Unknown payment method: ${method}`);
return processor(...args);
}
Proxy Pattern
Intercept and customize operations on objects:
// Validation proxy
function createValidatedObject(target, validators) {
return new Proxy(target, {
set(obj, key, value) {
if (validators[key]) {
const error = validators[key](value);
if (error) throw new Error(`${key}: ${error}`);
}
obj[key] = value;
return true;
}
});
}
const user = createValidatedObject({}, {
age: v => v < 0 ? "Must be positive" : v > 150 ? "Unrealistic age" : null,
email: v => !v.includes("@") ? "Invalid email" : null
});
user.age = 25; // OK
user.age = -5; // Error: age: Must be positive
user.email = "alice@example.com"; // OK
Decorator Pattern
Add behavior to objects without changing their class — JavaScript decorators (@) do this at the syntax level:
// Manual decorator (wrapper function)
function withLogging(fn, name = fn.name) {
return function(...args) {
console.time(name);
try {
const result = fn.apply(this, args);
return result;
} finally {
console.timeEnd(name);
}
};
}
const expensiveOp = withLogging(function expensiveOp(n) {
return fibonacci(n);
});
// Method decorators (Stage 3 proposal)
class Api {
@retry({ times: 3, delay: 1000 })
@cache({ ttl: 60 })
async getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
}
The Revealing Module Pattern
const cartModule = (() => {
// Private
let items = [];
function calculate() {
return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}
// Public
return {
add: (item) => { items.push(item); },
remove: (id) => { items = items.filter(i => i.id !== id); },
total: calculate,
clear: () => { items = []; }
};
})();
cartModule.add({ id: 1, price: 9.99, qty: 2 });
cartModule.total(); // 19.98
Next lesson: Node.js — JavaScript on the server side.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises