Design patterns are solutions to recurring software engineering problems, not rules to follow blindly. The Gang of Four catalogued 23 patterns in 1994. Most JavaScript developers use a subset daily without naming them. Naming and understanding them makes you more effective at recognizing problems and communicating solutions with other engineers.
⚡ TL;DR: Creational patterns (Singleton, Factory) create objects. Structural patterns (Proxy, Decorator, Module) compose objects. Behavioral patterns (Observer, Strategy, Command) define object interaction. JavaScript’s dynamic nature makes many patterns simpler than their Java/C++ implementations.
Singleton — one instance only
// Singleton: ensure only one instance exists globally
// Modern JavaScript: ES modules are singletons by default!
// db.js
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export default pool; // Imported anywhere: always same pool instance
// The classic class-based singleton:
class Logger {
static instance;
constructor() {
if (Logger.instance) return Logger.instance;
Logger.instance = this;
this.logs = [];
}
log(msg) { this.logs.push({ msg, at: Date.now() }); }
}
const a = new Logger();
const b = new Logger();
a === b; // true — same instance
// When to use: shared resource (DB pool, logger, config)
// When NOT to use: when it creates hidden global state that makes testing hard
Factory — create objects without specifying class
// Factory: centralize object creation logic
class NotificationFactory {
static create(type, payload) {
switch (type) {
case 'email': return new EmailNotification(payload);
case 'sms': return new SMSNotification(payload);
case 'push': return new PushNotification(payload);
case 'webhook': return new WebhookNotification(payload);
default: throw new Error(`Unknown notification type: ${type}`);
}
}
}
// Usage: caller doesn't know which class is instantiated
const notif = NotificationFactory.create('email', { to: 'alice@example.com', body: '...' });
await notif.send();
// Abstract Factory: factory of factories
// Strategy: select algorithm at runtime (see below)
Observer — event-driven decoupling
// Observer: objects subscribe to events from a subject
// JavaScript native: EventEmitter (Node.js), EventTarget (browser)
class EventEmitter {
#listeners = new Map();
on(event, handler) {
if (!this.#listeners.has(event)) this.#listeners.set(event, new Set());
this.#listeners.get(event).add(handler);
return () => this.off(event, handler); // Returns unsubscribe function
}
off(event, handler) {
this.#listeners.get(event)?.delete(handler);
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach(h => h(...args));
}
}
const store = new EventEmitter();
const unsubscribe = store.on('user:updated', (user) => updateUI(user));
// Later: unsubscribe() to avoid memory leaks
Strategy — swap algorithms at runtime
// Strategy: define family of algorithms, make them interchangeable
// Instead of:
function sort(arr, type) {
if (type === 'bubble') { /* ... */ }
else if (type === 'quick') { /* ... */ }
// Growing if/else = code smell
}
// Strategy pattern:
const sortStrategies = {
bubble: (arr) => { /* bubble sort */ },
quick: (arr) => { /* quick sort */ },
merge: (arr) => { /* merge sort */ },
};
class Sorter {
#strategy;
constructor(strategy) { this.#strategy = strategy; }
setStrategy(strategy) { this.#strategy = strategy; } // Swap at runtime
sort(data) { return this.#strategy(data); }
}
const sorter = new Sorter(sortStrategies.quick);
sorter.sort(data);
sorter.setStrategy(sortStrategies.merge); // Change algorithm without changing code
Command — encapsulate operations for undo/redo
// Command: encapsulate action as object — enables undo/redo, queuing, logging
class CommandHistory {
#history = [];
#redoStack = [];
execute(command) {
command.execute();
this.#history.push(command);
this.#redoStack = []; // Clear redo on new action
}
undo() {
const command = this.#history.pop();
if (command) { command.undo(); this.#redoStack.push(command); }
}
redo() {
const command = this.#redoStack.pop();
if (command) { command.execute(); this.#history.push(command); }
}
}
// Each action is a command:
class UpdateTextCommand {
constructor(editor, newText) {
this.editor = editor;
this.newText = newText;
this.oldText = editor.getText(); // Save for undo
}
execute() { this.editor.setText(this.newText); }
undo() { this.editor.setText(this.oldText); }
}
Design patterns quick reference
- Singleton: global shared resource. JS modules are singletons naturally.
- Factory: centralized object creation. Replaces type-switching new calls.
- Observer: event-driven communication. Node EventEmitter, DOM events.
- Strategy: swappable algorithms. Replaces if/else or switch type checks.
- Command: encapsulate actions. Enables undo/redo, queuing, audit logs.
- Proxy: intercept operations. ES6 Proxy for validation, logging, lazy loading.
- Decorator: add behavior without subclassing. Function wrappers, middleware.
- Module: encapsulate private state. ES6 modules + closure pattern.
The Proxy pattern in JavaScript is covered in depth in the JavaScript Proxy deep dive. The Observer pattern powers the reactive systems described in the React hooks guide. External reference: Refactoring Guru — Design Patterns in JavaScript.
Recommended Reading
→ Designing Data-Intensive Applications — The essential book every senior developer needs. Covers distributed systems, databases, and production architecture.
→ The Pragmatic Programmer — Timeless engineering wisdom for writing better, more maintainable code at any level.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Join 1,000+ senior developers getting expert-level JS, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
