Design patterns are reusable solutions to common problems in software design. They're not code you copy — they're blueprints you adapt. Knowing them helps you write cleaner code and communicate clearly with other developers ("this is a Factory pattern" conveys a lot).
This guide covers the most practical JavaScript patterns with real examples you can use today.
Quick Reference
| Pattern | Category | Problem It Solves |
|---|---|---|
| Singleton | Creational | One shared instance (config, cache) |
| Factory | Creational | Create objects without specifying exact class |
| Builder | Creational | Construct complex objects step by step |
| Observer | Behavioral | Notify many objects when state changes |
| Strategy | Behavioral | Swap algorithms at runtime |
| Command | Behavioral | Encapsulate actions as objects (undo/redo) |
| Module | Structural | Encapsulate private state |
| Decorator | Structural | Add behavior without modifying original |
| Facade | Structural | Simplify a complex subsystem |
| Proxy | Structural | Control access to an object |
Creational Patterns
Singleton — One Instance Only
Ensures a class has only one instance and provides a global access point. Common for config stores, connection pools, and caches.
class Config {
#data = {};
static #instance = null;
static getInstance() {
if (!Config.#instance) {
Config.#instance = new Config();
}
return Config.#instance;
}
set(key, value) {
this.#data[key] = value;
return this;
}
get(key) {
return this.#data[key];
}
}
// Always returns the same object
const config1 = Config.getInstance();
const config2 = Config.getInstance();
config1.set('theme', 'dark');
console.log(config2.get('theme')); // 'dark'
console.log(config1 === config2); // true
Module-level singleton (simpler, idiomatic in JS):
// config.js — imported as a singleton by Node's module cache
export const config = {
apiUrl: process.env.API_URL,
timeout: 5000,
};
When to use: Shared state that must be consistent across your app. Avoid when it makes testing hard (global mutable state is a code smell).
Factory — Create Without Specifying the Class
A function or method that creates objects without exposing the exact construction logic.
function createUser(role) {
const base = { role, createdAt: new Date() };
switch (role) {
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: '/home' };
default:
throw new Error(`Unknown role: ${role}`);
}
}
const admin = createUser('admin');
const viewer = createUser('viewer');
Class-based factory with a registry:
class NotificationFactory {
static #types = new Map();
static register(type, cls) {
this.#types.set(type, cls);
}
static create(type, options) {
const Cls = this.#types.get(type);
if (!Cls) throw new Error(`Unknown notification type: ${type}`);
return new Cls(options);
}
}
class EmailNotification {
constructor({ to, subject }) { this.to = to; this.subject = subject; }
send() { console.log(`Email to ${this.to}: ${this.subject}`); }
}
class SMSNotification {
constructor({ phone, message }) { this.phone = phone; this.message = message; }
send() { console.log(`SMS to ${this.phone}: ${this.message}`); }
}
NotificationFactory.register('email', EmailNotification);
NotificationFactory.register('sms', SMSNotification);
const n = NotificationFactory.create('email', { to: 'user@example.com', subject: 'Hello' });
n.send();
When to use: When object creation logic is complex or varies based on a type/condition.
Builder — Step-by-Step Construction
Separates the construction of a complex object from its representation.
class QueryBuilder {
#table = '';
#conditions = [];
#columns = ['*'];
#limit = null;
#orderBy = null;
from(table) {
this.#table = table;
return this; // enables chaining
}
select(...columns) {
this.#columns = columns;
return this;
}
where(condition) {
this.#conditions.push(condition);
return this;
}
orderBy(column, direction = 'ASC') {
this.#orderBy = `${column} ${direction}`;
return this;
}
take(n) {
this.#limit = n;
return this;
}
build() {
if (!this.#table) throw new Error('Table is required');
let sql = `SELECT ${this.#columns.join(', ')} FROM ${this.#table}`;
if (this.#conditions.length) sql += ` WHERE ${this.#conditions.join(' AND ')}`;
if (this.#orderBy) sql += ` ORDER BY ${this.#orderBy}`;
if (this.#limit !== null) sql += ` LIMIT ${this.#limit}`;
return sql;
}
}
const query = new QueryBuilder()
.from('users')
.select('id', 'name', 'email')
.where('active = true')
.where('age > 18')
.orderBy('name')
.take(10)
.build();
// SELECT id, name, email FROM users WHERE active = true AND age > 18 ORDER BY name ASC LIMIT 10
When to use: Objects with many optional parameters — avoids telescoping constructors.
Behavioral Patterns
Observer — Publish/Subscribe
Defines a one-to-many dependency: when one object changes, all dependents are notified automatically.
class EventEmitter {
#listeners = new Map();
on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(callback);
return () => this.off(event, callback); // returns unsubscribe fn
}
off(event, callback) {
this.#listeners.get(event)?.delete(callback);
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach(cb => cb(...args));
}
}
// Usage
const store = new EventEmitter();
const unsub = store.on('cart:updated', (cart) => {
console.log('Cart updated:', cart.items.length, 'items');
});
store.emit('cart:updated', { items: ['apple', 'banana'] });
// Cart updated: 2 items
unsub(); // clean up listener
When to use: React to state changes across loosely coupled components. Native DOM events follow this pattern.
Strategy — Swap Algorithms at Runtime
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
// Strategies
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[arr.length - 1];
const left = arr.slice(0, -1).filter(x => x <= pivot);
const right = arr.slice(0, -1).filter(x => x > pivot);
return [...sortStrategies.quick(left), pivot, ...sortStrategies.quick(right)];
},
native(arr) {
return [...arr].sort((a, b) => a - b);
},
};
class Sorter {
#strategy;
constructor(strategy = 'native') {
this.#strategy = sortStrategies[strategy];
}
setStrategy(name) {
if (!sortStrategies[name]) throw new Error(`Unknown strategy: ${name}`);
this.#strategy = sortStrategies[name];
return this;
}
sort(data) {
return this.#strategy(data);
}
}
const sorter = new Sorter('native');
console.log(sorter.sort([3, 1, 4, 1, 5, 9])); // [1, 1, 3, 4, 5, 9]
sorter.setStrategy('quick');
console.log(sorter.sort([3, 1, 4, 1, 5, 9])); // [1, 1, 3, 4, 5, 9]
Real-world example: Choosing a payment processor (Stripe vs PayPal), choosing a compression algorithm, switching between auth methods.
Command — Encapsulate Actions as Objects
Turns requests into standalone objects. Enables undo/redo, queuing, and logging.
class TextEditor {
#content = '';
#history = [];
execute(command) {
command.execute(this);
this.#history.push(command);
}
undo() {
const command = this.#history.pop();
command?.undo(this);
}
getContent() { return this.#content; }
setContent(text) { this.#content = text; }
}
class InsertCommand {
#text;
#position;
#previous;
constructor(text, position) {
this.#text = text;
this.#position = position;
}
execute(editor) {
this.#previous = editor.getContent();
const content = editor.getContent();
editor.setContent(
content.slice(0, this.#position) + this.#text + content.slice(this.#position)
);
}
undo(editor) {
editor.setContent(this.#previous);
}
}
const editor = new TextEditor();
editor.execute(new InsertCommand('Hello', 0));
editor.execute(new InsertCommand(', world', 5));
console.log(editor.getContent()); // Hello, world
editor.undo();
console.log(editor.getContent()); // Hello
editor.undo();
console.log(editor.getContent()); // ''
Structural Patterns
Module Pattern — Encapsulate Private State
Groups related code and hides implementation details. Modern ES modules do this natively, but the pattern also works in non-module contexts.
// ES module as a module pattern (preferred)
// cart.js
let items = [];
export function addItem(item) {
items = [...items, item]; // immutable update
}
export function removeItem(id) {
items = items.filter(i => i.id !== id);
}
export function getItems() {
return [...items]; // return copy, not reference
}
export function getTotal() {
return items.reduce((sum, i) => sum + i.price, 0);
}
The items array is private — consumers can only interact through the exported functions.
Decorator — Add Behavior Without Modifying the Original
Wraps an object to extend its behavior. JavaScript's higher-order functions make this natural.
// Function decorator: add logging
function withLogging(fn, name = fn.name) {
return function (...args) {
console.log(`[${name}] called with`, args);
const result = fn.apply(this, args);
console.log(`[${name}] returned`, result);
return result;
};
}
function add(a, b) { return a + b; }
const loggedAdd = withLogging(add);
loggedAdd(2, 3); // logs call and result
// Async decorator: add retry logic
function withRetry(fn, retries = 3, delay = 500) {
return async function (...args) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn.apply(this, args);
} catch (err) {
if (attempt === retries) throw err;
await new Promise(r => setTimeout(r, delay * attempt));
}
}
};
}
const fetchWithRetry = withRetry(fetch, 3, 1000);
const res = await fetchWithRetry('https://api.example.com/data');
TC39 Decorator proposal (Stage 3, supported in TypeScript):
function readonly(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
return descriptor;
}
class Circle {
@readonly
area() { return Math.PI * this.radius ** 2; }
}
Facade — Simplify a Complex Subsystem
Provides a simple interface to a complex set of interfaces.
// Complex subsystem
class AudioDecoder { decode(file) { /* ... */ } }
class VideoDecoder { decode(file) { /* ... */ } }
class Renderer { render(video, audio) { /* ... */ } }
class BufferManager { allocate(size) { /* ... */ } }
// Facade
class MediaPlayer {
#audio = new AudioDecoder();
#video = new VideoDecoder();
#renderer = new Renderer();
#buffer = new BufferManager();
play(file) {
this.#buffer.allocate(file.size);
const audio = this.#audio.decode(file);
const video = this.#video.decode(file);
this.#renderer.render(video, audio);
}
}
// Client only needs the facade
const player = new MediaPlayer();
player.play(myVideoFile);
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Singleton for everything | Hidden global state, hard to test | Use dependency injection instead |
| Observer without cleanup | Memory leaks from accumulated listeners | Always return and call unsubscribe |
Factory that returns null |
Callers must check null everywhere | Throw on unknown type instead |
| Mutable state in Module | External code can still mutate returned objects | Return copies or use Object.freeze |
| Decorator that changes signature | Breaks callers that expect original API | Decorators should be transparent |
| Over-engineering with patterns | Added complexity for simple code | Patterns solve problems — apply only when the problem exists |
Choosing the Right Pattern
Need exactly one instance? → Singleton
Creating objects with complex logic? → Factory or Builder
React to state changes? → Observer
Swap algorithms at runtime? → Strategy
Need undo/redo or queueing? → Command
Hide private state? → Module
Add behavior without changing code? → Decorator
Simplify a complex API? → Facade
Control/intercept access? → Proxy
FAQ
Are design patterns language-specific? No. The GoF book used C++ and Smalltalk, but patterns apply in any OO or functional language. JavaScript patterns often look different than Java patterns due to first-class functions and prototypal inheritance.
Should I memorize all 23 GoF patterns? Focus on the ones you'll actually use: Singleton, Factory, Observer, Strategy, Module, Decorator, and Facade cover 90% of practical cases. Learn others when you encounter the specific problem they solve.
Is the Singleton pattern bad? It's controversial because it introduces global mutable state and makes testing hard. Prefer constructor injection or module-level singletons (which are testable by mocking the module). Use Singleton only when you truly need one shared instance.
What's the difference between Observer and Pub/Sub?
Observer: subjects and observers are directly linked (subject knows its observers). Pub/Sub: a message broker sits in between — publishers don't know subscribers. Node's EventEmitter is Observer; BroadcastChannel or Redis pub/sub is Pub/Sub.
How are design patterns different from architectural patterns? Design patterns (like Observer, Factory) operate at the class/function level. Architectural patterns (MVC, MVVM, Event Sourcing, CQRS) operate at the system or application level.
Do functional programming patterns replace OOP patterns? Partially. FP's higher-order functions replace many creational patterns (Factory becomes a function). But patterns like Observer, Command, and Strategy translate naturally to FP (they're already about functions). React hooks are functional takes on Observer.