Design patterns are reusable solutions to recurring software design problems. They are not copy-paste code โ they are templates for structuring code that have been validated across thousands of codebases. Understanding patterns helps you communicate intent clearly with other developers, recognise familiar structures in frameworks and libraries you use daily, and make deliberate architectural decisions rather than accidental ones. In this lesson you will master the most important JavaScript patterns โ creational, structural, and behavioural โ with idiomatic modern implementations and real-world use cases for each.
Creational Patterns
| Pattern | Intent | JS Idiom |
|---|---|---|
| Factory Function | Create objects without new |
Function returning an object literal or closure |
| Factory Method | Subclasses decide what to instantiate | Static create() method; switch on type |
| Singleton | One instance, globally accessible | Module-level variable exported once |
| Builder | Construct complex objects step by step | Method-chaining class; returns this |
Structural Patterns
| Pattern | Intent | JS Idiom |
|---|---|---|
| Decorator | Add behaviour without modifying the original | Higher-order function wrapping a function / class |
| Facade | Simplify a complex API with a unified interface | Module with a clean public API over internal complexity |
| Adapter | Make incompatible interfaces compatible | Wrapper function converting one API shape to another |
| Composite | Treat single objects and collections uniformly | Recursive tree where leaves and branches share an interface |
Behavioural Patterns
| Pattern | Intent | JS Idiom |
|---|---|---|
| Observer / PubSub | Notify subscribers of events | EventEmitter class; CustomEvent; reactive signals |
| Strategy | Swap algorithms at runtime | Function passed as argument or stored in an object |
| Command | Encapsulate actions as objects โ undo/redo | Class with execute() and undo() |
| Middleware / Chain of Responsibility | Pass request through a pipeline | Array of functions, each calling next() |
getInstance() method โ just export a constant from a module.Basic Example
// โโ Factory Function โ create without new โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function createUser(name, email, role = 'user') {
let loginCount = 0; // private via closure
return {
name,
email,
role,
login() { loginCount++; return this; },
getStats() { return { name, loginCount }; },
};
}
const alice = createUser('Alice', 'alice@example.com', 'admin');
alice.login().login();
console.log(alice.getStats()); // { name: 'Alice', loginCount: 2 }
// โโ Singleton โ module-level instance โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// config.js โ one instance shared by all importers
let _config = { theme: 'light', lang: 'en' };
export const config = {
get(key) { return _config[key]; },
set(key, val) { _config = { ..._config, [key]: val }; },
getAll() { return { ..._config }; },
};
// โโ Builder โ step-by-step construction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class QueryBuilder {
#table = '';
#conditions = [];
#columns = ['*'];
#limit = null;
#orderBy = null;
from(table) { this.#table = table; return this; }
select(...cols) { this.#columns = cols; return this; }
where(cond) { this.#conditions.push(cond); return this; }
limit(n) { this.#limit = n; return this; }
orderBy(col, dir='ASC') { this.#orderBy = `${col} ${dir}`; 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("role = 'admin'")
.where('active = true')
.orderBy('name')
.limit(10)
.build();
console.log(query);
// SELECT id, name, email FROM users WHERE role = 'admin' AND active = true ORDER BY name ASC LIMIT 10
// โโ Observer / EventEmitter โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class EventEmitter {
#events = new Map();
on(event, fn) {
if (!this.#events.has(event)) this.#events.set(event, new Set());
this.#events.get(event).add(fn);
return () => this.off(event, fn);
}
once(event, fn) {
const wrapper = (...args) => { fn(...args); this.off(event, wrapper); };
return this.on(event, wrapper);
}
off(event, fn) {
this.#events.get(event)?.delete(fn);
}
emit(event, ...args) {
this.#events.get(event)?.forEach(fn => fn(...args));
}
}
const store = new EventEmitter();
const unsub = store.on('change', data => console.log('Changed:', data));
store.emit('change', { key: 'theme', value: 'dark' });
unsub();
// โโ Strategy โ swappable algorithm โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function sortItems(items, strategy = 'byName') {
const strategies = {
byName: (a, b) => a.name.localeCompare(b.name),
byPrice: (a, b) => a.price - b.price,
byDate: (a, b) => new Date(a.date) - new Date(b.date),
};
const compareFn = typeof strategy === 'function'
? strategy
: strategies[strategy] ?? strategies.byName;
return [...items].sort(compareFn);
}
const items = [
{ name: 'Widget', price: 9.99, date: '2024-01-15' },
{ name: 'Gadget', price: 49.00, date: '2024-03-02' },
{ name: 'Doohickey', price: 4.99, date: '2024-02-10' },
];
console.log(sortItems(items, 'byPrice').map(i => i.name)); // Doohickey, Widget, Gadget
console.log(sortItems(items, 'byDate').map(i => i.name)); // Widget, Doohickey, Gadget
// โโ Middleware / Pipe โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function compose(...fns) {
return fns.reduce((f, g) => (...args) => f(g(...args)));
}
function pipe(...fns) {
return fns.reduce((f, g) => (...args) => g(f(...args)));
}
const process = pipe(
str => str.trim(),
str => str.toLowerCase(),
str => str.replace(/\s+/g, '-'),
str => encodeURIComponent(str),
);
console.log(process(' Hello World ')); // 'hello-world'
// โโ Command pattern โ undo/redo โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class CommandHistory {
#history = [];
#redoStack = [];
execute(command) {
command.execute();
this.#history.push(command);
this.#redoStack = []; // clear redo on new command
}
undo() {
const cmd = this.#history.pop();
if (cmd) { cmd.undo(); this.#redoStack.push(cmd); }
}
redo() {
const cmd = this.#redoStack.pop();
if (cmd) { cmd.execute(); this.#history.push(cmd); }
}
}
class SetValueCommand {
#target; #key; #newVal; #oldVal;
constructor(target, key, newVal) {
this.#target = target;
this.#key = key;
this.#newVal = newVal;
}
execute() { this.#oldVal = this.#target[this.#key]; this.#target[this.#key] = this.#newVal; }
undo() { this.#target[this.#key] = this.#oldVal; }
}
const state = { count: 0 };
const history = new CommandHistory();
history.execute(new SetValueCommand(state, 'count', 5));
history.execute(new SetValueCommand(state, 'count', 10));
console.log(state.count); // 10
history.undo();
console.log(state.count); // 5
history.undo();
console.log(state.count); // 0
history.redo();
console.log(state.count); // 5
How It Works
Step 1 โ Factory Functions Use Closures for Privacy
Variables declared inside a factory function are captured by the returned object’s methods via closure. They are inaccessible from outside โ more private than class private fields, which can at least be accessed by the class itself. Factory functions also naturally support composition โ you build objects by mixing functions, not by navigating inheritance hierarchies.
Step 2 โ Builder Pattern Enables Readable Fluent APIs
Each method returns this, enabling method chaining. The builder accumulates configuration across multiple calls, then a terminal method (build(), execute()) validates and uses the configuration. This pattern makes complex configuration readable and discoverable โ the API documents itself through method names.
Step 3 โ Observer Decouples Publishers from Subscribers
An EventEmitter stores subscriber functions in a Map of Sets. emit() invokes all subscribers for an event. Neither the emitter nor the subscribers need references to each other โ they communicate only through event names and payloads. This loose coupling is the foundation of reactive UI patterns and the DOM’s own event system.
Step 4 โ Strategy Pattern Is Just a Function Parameter
In JavaScript, the Strategy pattern is often just passing a function as an argument โ no classes required. The “strategy” is a comparison function, a formatter, a validator โ any swappable algorithm. Supporting both string aliases and raw functions (as in the sorting example) makes the API both convenient and fully flexible.
Step 5 โ Command Pattern Enables Undo/Redo
Encapsulating operations as Command objects (with execute and undo) makes the history stack possible. Each command stores the state it needs to reverse itself. The history manages the stack, making undo/redo a property of the system rather than each individual operation. This pattern is used in text editors, drawing apps, and any stateful UI.
Real-World Example: Request Pipeline (Middleware)
// middleware-pipeline.js
class Pipeline {
#middleware = [];
use(fn) {
this.#middleware.push(fn);
return this;
}
async execute(context) {
const run = async (index, ctx) => {
if (index >= this.#middleware.length) return ctx;
const fn = this.#middleware[index];
const next = ctx => run(index + 1, ctx);
return fn(ctx, next);
};
return run(0, context);
}
}
// Middleware functions โ each receives context and next()
const logger = async (ctx, next) => {
console.log(`[${new Date().toISOString()}] ${ctx.method} ${ctx.path}`);
const result = await next(ctx);
console.log(`[done] ${ctx.path} -> ${result.status}`);
return result;
};
const auth = async (ctx, next) => {
if (!ctx.token) return { ...ctx, status: 401, body: 'Unauthorized' };
return next({ ...ctx, user: decodeToken(ctx.token) });
};
const rateLimit = (maxPerMinute) => {
const counts = new Map();
return async (ctx, next) => {
const key = ctx.ip;
const now = Date.now();
const entry = counts.get(key) ?? { count: 0, reset: now + 60_000 };
if (now > entry.reset) { entry.count = 0; entry.reset = now + 60_000; }
if (++entry.count > maxPerMinute) {
return { ...ctx, status: 429, body: 'Too Many Requests' };
}
counts.set(key, entry);
return next(ctx);
};
};
const api = new Pipeline()
.use(logger)
.use(rateLimit(60))
.use(auth)
.use(async (ctx, next) => {
// Actual handler
return { ...ctx, status: 200, body: { user: ctx.user } };
});
const result = await api.execute({
method: 'GET',
path: '/api/me',
ip: '127.0.0.1',
token: 'eyJhbGc...',
});
console.log(result.status, result.body);
Common Mistakes
Mistake 1 โ Over-engineering with patterns when simple code suffices
โ Wrong โ Factory + Builder + Strategy for a two-field config:
const config = ConfigBuilderFactory.create()
.withStrategy(new DefaultStrategy())
.withTheme('dark')
.build();
โ Correct โ use the simplest expression:
const config = { theme: 'dark', lang: 'en' };
Mistake 2 โ Singleton via class instead of module export
โ Unnecessary boilerplate:
class Database {
static #instance;
static getInstance() {
return this.#instance ?? (this.#instance = new Database());
}
}
โ Idiomatic JS โ ES module is a natural singleton:
// db.js
export const db = new Database(process.env.DB_URL);
// Every importer gets the same db instance
Mistake 3 โ Observer without cleanup โ memory leak
โ Wrong โ listener never removed:
store.on('change', updateUI); // component unmounts โ listener stays forever
โ Correct โ store unsubscribe fn and call it on cleanup:
const unsub = store.on('change', updateUI);
// On component unmount:
unsub();
Quick Reference
| Pattern | One-line summary |
|---|---|
| Factory Function | Function that returns an object โ closures for private state |
| Singleton | ES module export โ loaded once, shared everywhere |
| Builder | Methods return this for chaining โ terminal method creates result |
| Observer | Map of event name โ Set of handlers; emit calls all handlers |
| Strategy | Pass a function โ swap algorithm without changing the caller |
| Decorator | Wrap a function to add behaviour before/after the original |
| Command | Object with execute() and undo() โ history stack enables undo/redo |
| Middleware | Array of functions each receiving context and next() |