Design Patterns in JavaScript

โ–ถ Try It Yourself

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()
Note: JavaScript’s first-class functions and closures mean many patterns that require full class hierarchies in Java or C++ can be expressed concisely as functions. The Strategy pattern โ€” swapping algorithms โ€” is just passing a function as an argument. The Decorator pattern โ€” adding behaviour โ€” is a higher-order function. Recognise the intent of the pattern, then choose the most idiomatic JavaScript expression of it.
Tip: ES modules are the idiomatic JavaScript Singleton. A module is loaded and executed exactly once regardless of how many files import it. If you export a single instance, every importer gets the same instance. You do not need a class with a static getInstance() method โ€” just export a constant from a module.
Warning: Patterns can be overused. Wrapping a simple two-line function in a Strategy class or adding an Observer to two tightly-coupled components adds indirection without benefit. Apply patterns when they solve a real problem you have โ€” not because they sound impressive. The best code is the simplest code that clearly expresses intent.

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();

▶ Try It Yourself

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()

🧠 Test Yourself

Which pattern is most appropriate when you need to run a request through authentication, logging, and rate-limiting steps in sequence?





โ–ถ Try It Yourself