Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
20 minLesson 29 of 35
OOP & Advanced Concepts

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

Get Notes Free →
!