JavaScript — Design Patterns
Design patterns are reusable solutions to common problems in software design. They represent formalized best practices that developers can apply to solve recurring architectural challenges. In JavaScript, patterns must account for the language's unique features — first-class functions, prototypal inheritance, dynamic typing, and single-threaded event loop.
Patterns are not plug-and-play code snippets. They are templates for how to structure your code. Applying a pattern correctly requires understanding the problem it solves, the trade-offs it introduces, and whether simpler solutions exist. JavaScript's flexibility means many patterns are either built into the language or require less ceremony than in statically-typed languages.
This guide covers the most relevant design patterns in JavaScript, organized by category. Each pattern includes a real-world use case, implementation example, and guidance on when to apply it. The overarching principle: patterns exist to solve problems — if there is no problem, a pattern is unnecessary overhead.
Creational patterns deal with object creation mechanisms. They abstract the instantiation process, making a system independent of how its objects are created, composed, and represented.
Module Pattern
The Module pattern encapsulates private state and exposes a public API. Before ES modules, this was the primary way to create privacy in JavaScript using closures and IIFEs. The revealing module pattern is a variation that returns an object with references to selected private functions.
| 1 | // Classic Module Pattern (IIFE) |
| 2 | const CounterModule = (function() { |
| 3 | // Private state |
| 4 | let count = 0; |
| 5 | |
| 6 | // Private function |
| 7 | function validate(n) { |
| 8 | return n >= 0; |
| 9 | } |
| 10 | |
| 11 | // Public API |
| 12 | return { |
| 13 | increment() { |
| 14 | count++; |
| 15 | return count; |
| 16 | }, |
| 17 | decrement() { |
| 18 | if (validate(count - 1)) { |
| 19 | count--; |
| 20 | } |
| 21 | return count; |
| 22 | }, |
| 23 | getCount() { |
| 24 | return count; |
| 25 | }, |
| 26 | reset() { |
| 27 | count = 0; |
| 28 | } |
| 29 | }; |
| 30 | })(); |
| 31 | |
| 32 | console.log(CounterModule.getCount()); // 0 |
| 33 | CounterModule.increment(); |
| 34 | CounterModule.increment(); |
| 35 | console.log(CounterModule.getCount()); // 2 |
| 36 | // CounterModule.count — undefined (private) |
| 37 | |
| 38 | // Revealing Module Pattern |
| 39 | const UserStore = (function() { |
| 40 | const users = new Map(); |
| 41 | |
| 42 | function addUser(user) { |
| 43 | if (!user.id) throw new Error('User must have an id'); |
| 44 | users.set(user.id, user); |
| 45 | } |
| 46 | |
| 47 | function getUser(id) { |
| 48 | return users.get(id); |
| 49 | } |
| 50 | |
| 51 | function listUsers() { |
| 52 | return Array.from(users.values()); |
| 53 | } |
| 54 | |
| 55 | // Reveal pointers to private functions |
| 56 | return { |
| 57 | add: addUser, |
| 58 | get: getUser, |
| 59 | list: listUsers |
| 60 | }; |
| 61 | })(); |
Singleton Pattern
The Singleton ensures a class has only one instance and provides a global access point. In JavaScript, modules are naturally singletons when imported. The pattern is useful for shared state, configuration, and service objects — but can introduce hidden dependencies and make testing difficult.
| 1 | // Singleton via module (ES modules are singletons by default) |
| 2 | // config.js |
| 3 | let instance = null; |
| 4 | |
| 5 | class Config { |
| 6 | constructor() { |
| 7 | if (instance) return instance; |
| 8 | this.settings = {}; |
| 9 | instance = this; |
| 10 | } |
| 11 | |
| 12 | get(key) { |
| 13 | return this.settings[key]; |
| 14 | } |
| 15 | |
| 16 | set(key, value) { |
| 17 | this.settings[key] = value; |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | export default new Config(); |
| 22 | |
| 23 | // Simple singleton with module-scoped state |
| 24 | let dbConnection = null; |
| 25 | |
| 26 | export async function getDatabase() { |
| 27 | if (!dbConnection) { |
| 28 | dbConnection = await createConnection(process.env.DATABASE_URL); |
| 29 | } |
| 30 | return dbConnection; |
| 31 | } |
| 32 | |
| 33 | // Singleton with lazy initialization |
| 34 | class Logger { |
| 35 | constructor() { |
| 36 | if (Logger._instance) return Logger._instance; |
| 37 | this.logs = []; |
| 38 | Logger._instance = this; |
| 39 | } |
| 40 | |
| 41 | log(message) { |
| 42 | this.logs.push({ message, timestamp: Date.now() }); |
| 43 | console.log(`[LOG] ${message}`); |
| 44 | } |
| 45 | |
| 46 | getLogs() { |
| 47 | return [...this.logs]; |
| 48 | } |
| 49 | |
| 50 | static getInstance() { |
| 51 | if (!Logger._instance) { |
| 52 | new Logger(); |
| 53 | } |
| 54 | return Logger._instance; |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | const logger1 = new Logger(); |
| 59 | const logger2 = new Logger(); |
| 60 | console.log(logger1 === logger2); // true |
Factory Pattern
The Factory pattern provides an interface for creating objects without specifying the exact class. It abstracts instantiation logic and is useful when object creation is complex, conditional, or should be centralized. Factory methods can return different object types based on input.
| 1 | // Simple factory function |
| 2 | function createUser(type, data) { |
| 3 | switch (type) { |
| 4 | case 'admin': |
| 5 | return { role: 'admin', permissions: ['read', 'write', 'delete'], ...data }; |
| 6 | case 'editor': |
| 7 | return { role: 'editor', permissions: ['read', 'write'], ...data }; |
| 8 | case 'viewer': |
| 9 | return { role: 'viewer', permissions: ['read'], ...data }; |
| 10 | default: |
| 11 | throw new Error(`Unknown user type: ${type}`); |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | const admin = createUser('admin', { name: 'Alice', id: 1 }); |
| 16 | const editor = createUser('editor', { name: 'Bob', id: 2 }); |
| 17 | |
| 18 | // Factory with class hierarchy |
| 19 | class Notification { |
| 20 | constructor(recipient, message) { |
| 21 | this.recipient = recipient; |
| 22 | this.message = message; |
| 23 | } |
| 24 | send() { throw new Error('Not implemented'); } |
| 25 | } |
| 26 | |
| 27 | class EmailNotification extends Notification { |
| 28 | send() { |
| 29 | console.log(`Email to ${this.recipient}: ${this.message}`); |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | class SMSNotification extends Notification { |
| 34 | send() { |
| 35 | console.log(`SMS to ${this.recipient}: ${this.message}`); |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | class PushNotification extends Notification { |
| 40 | send() { |
| 41 | console.log(`Push to ${this.recipient}: ${this.message}`); |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | // Factory class |
| 46 | class NotificationFactory { |
| 47 | static create(type, recipient, message) { |
| 48 | switch (type) { |
| 49 | case 'email': return new EmailNotification(recipient, message); |
| 50 | case 'sms': return new SMSNotification(recipient, message); |
| 51 | case 'push': return new PushNotification(recipient, message); |
| 52 | default: throw new Error(`Unknown type: ${type}`); |
| 53 | } |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | const notif = NotificationFactory.create('email', 'alice@example.com', 'Hello!'); |
| 58 | notif.send(); |
info
Structural patterns concern class and object composition. They describe ways to assemble objects and classes into larger structures while keeping the system flexible and efficient.
Decorator Pattern
The Decorator pattern attaches additional responsibilities to an object dynamically. In JavaScript, this maps naturally to higher-order functions that wrap behavior. The pattern is useful for cross-cutting concerns like logging, caching, authentication, and validation.
| 1 | // Function decorator pattern |
| 2 | function withLogging(fn) { |
| 3 | return function(...args) { |
| 4 | console.log(`Calling ${fn.name} with`, args); |
| 5 | const result = fn.apply(this, args); |
| 6 | console.log(`Result:`, result); |
| 7 | return result; |
| 8 | }; |
| 9 | } |
| 10 | |
| 11 | function withTiming(fn) { |
| 12 | return function(...args) { |
| 13 | const start = performance.now(); |
| 14 | const result = fn.apply(this, args); |
| 15 | const elapsed = performance.now() - start; |
| 16 | console.log(`${fn.name} took ${elapsed.toFixed(2)}ms`); |
| 17 | return result; |
| 18 | }; |
| 19 | } |
| 20 | |
| 21 | function withCache(fn) { |
| 22 | const cache = new Map(); |
| 23 | return function(...args) { |
| 24 | const key = JSON.stringify(args); |
| 25 | if (cache.has(key)) { |
| 26 | console.log('Cache hit for', key); |
| 27 | return cache.get(key); |
| 28 | } |
| 29 | const result = fn.apply(this, args); |
| 30 | cache.set(key, result); |
| 31 | return result; |
| 32 | }; |
| 33 | } |
| 34 | |
| 35 | // Composing decorators |
| 36 | function fetchUser(id) { |
| 37 | // Simulate expensive operation |
| 38 | const users = { 1: 'Alice', 2: 'Bob' }; |
| 39 | return users[id] || null; |
| 40 | } |
| 41 | |
| 42 | const cachedFetchUser = withCache(withLogging(fetchUser)); |
| 43 | console.log(cachedFetchUser(1)); // Logs call + cache miss |
| 44 | console.log(cachedFetchUser(1)); // Logs call + cache hit |
| 45 | |
| 46 | // Class decorator (adding behavior to instances) |
| 47 | function withTimestamp(Base) { |
| 48 | return class extends Base { |
| 49 | constructor(...args) { |
| 50 | super(...args); |
| 51 | this.createdAt = new Date(); |
| 52 | } |
| 53 | }; |
| 54 | } |
| 55 | |
| 56 | @withTimestamp |
| 57 | class Document { |
| 58 | constructor(title) { |
| 59 | this.title = title; |
| 60 | } |
| 61 | } |
Mixin Pattern
Mixins are a way to compose behavior from multiple sources without classical inheritance. JavaScript's dynamic object system makes mixins trivial — you can copy methods from one prototype to another. They are useful for sharing behavior across unrelated classes.
| 1 | // Simple mixin via Object.assign |
| 2 | const CanLog = { |
| 3 | log(message) { |
| 4 | console.log(`[${this.constructor.name}] ${message}`); |
| 5 | }, |
| 6 | error(message) { |
| 7 | console.error(`[${this.constructor.name}] ERROR: ${message}`); |
| 8 | } |
| 9 | }; |
| 10 | |
| 11 | const CanEmit = { |
| 12 | _listeners: {}, |
| 13 | |
| 14 | on(event, handler) { |
| 15 | if (!this._listeners[event]) this._listeners[event] = []; |
| 16 | this._listeners[event].push(handler); |
| 17 | return this; |
| 18 | }, |
| 19 | |
| 20 | emit(event, ...args) { |
| 21 | const handlers = this._listeners[event] || []; |
| 22 | handlers.forEach(h => h(...args)); |
| 23 | return this; |
| 24 | } |
| 25 | }; |
| 26 | |
| 27 | // Apply mixins to a class |
| 28 | class User { |
| 29 | constructor(name) { |
| 30 | this.name = name; |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | Object.assign(User.prototype, CanLog, CanEmit); |
| 35 | |
| 36 | const user = new User('Alice'); |
| 37 | user.log('User created'); // [User] User created |
| 38 | user.on('login', () => console.log('Logged in!')); |
| 39 | user.emit('login'); // Logged in! |
| 40 | |
| 41 | // Functional mixin pattern |
| 42 | const withTimestamp = (Base) => class extends Base { |
| 43 | constructor(...args) { |
| 44 | super(...args); |
| 45 | this.createdAt = new Date(); |
| 46 | } |
| 47 | }; |
| 48 | |
| 49 | const withSerializable = (Base) => class extends Base { |
| 50 | toJSON() { |
| 51 | return JSON.stringify({ ...this }); |
| 52 | } |
| 53 | }; |
| 54 | |
| 55 | class Product {} |
| 56 | const EnhancedProduct = withSerializable(withTimestamp(Product)); |
| 57 | const p = new EnhancedProduct(); |
| 58 | console.log(p.createdAt); // Date object |
| 59 | console.log(p.toJSON()); // JSON string |
pro tip
Behavioral patterns focus on communication between objects. They describe how objects interact, distribute responsibility, and exchange data.
Observer Pattern
The Observer pattern defines a one-to-many dependency between objects. When the subject changes state, all its dependents (observers) are notified automatically. This is the foundation of reactive programming and is used in RxJS, Redux, and Vue's reactivity system.
| 1 | // Observer pattern implementation |
| 2 | class Subject { |
| 3 | constructor() { |
| 4 | this._observers = new Set(); |
| 5 | } |
| 6 | |
| 7 | subscribe(observer) { |
| 8 | this._observers.add(observer); |
| 9 | return () => this._observers.delete(observer); |
| 10 | } |
| 11 | |
| 12 | unsubscribe(observer) { |
| 13 | this._observers.delete(observer); |
| 14 | } |
| 15 | |
| 16 | notify(data) { |
| 17 | this._observers.forEach(observer => { |
| 18 | observer.update(data); |
| 19 | }); |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | class Observer { |
| 24 | constructor(name) { |
| 25 | this.name = name; |
| 26 | } |
| 27 | |
| 28 | update(data) { |
| 29 | console.log(`${this.name} received:`, data); |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | // Usage |
| 34 | const newsChannel = new Subject(); |
| 35 | |
| 36 | const alice = new Observer('Alice'); |
| 37 | const bob = new Observer('Bob'); |
| 38 | |
| 39 | const unsubscribeAlice = newsChannel.subscribe(alice); |
| 40 | newsChannel.subscribe(bob); |
| 41 | |
| 42 | newsChannel.notify({ headline: 'Breaking News!' }); |
| 43 | // Alice received: { headline: 'Breaking News!' } |
| 44 | // Bob received: { headline: 'Breaking News!' } |
| 45 | |
| 46 | unsubscribeAlice(); |
| 47 | newsChannel.notify({ headline: 'Update 2' }); |
| 48 | // Only Bob receives the update |
| 49 | |
| 50 | // Observable with function observers (simpler) |
| 51 | class EventBus { |
| 52 | constructor() { |
| 53 | this._handlers = new Map(); |
| 54 | } |
| 55 | |
| 56 | on(event, handler) { |
| 57 | if (!this._handlers.has(event)) { |
| 58 | this._handlers.set(event, new Set()); |
| 59 | } |
| 60 | this._handlers.get(event).add(handler); |
| 61 | return () => this._handlers.get(event).delete(handler); |
| 62 | } |
| 63 | |
| 64 | emit(event, data) { |
| 65 | const handlers = this._handlers.get(event); |
| 66 | if (handlers) { |
| 67 | handlers.forEach(h => h(data)); |
| 68 | } |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | const bus = new EventBus(); |
| 73 | bus.on('user:login', (user) => console.log(`${user.name} logged in`)); |
| 74 | bus.emit('user:login', { name: 'Alice' }); |
Pub/Sub Pattern
Publish/Subscribe (Pub/Sub) is a messaging pattern where senders (publishers) do not send messages directly to specific receivers (subscribers). Instead, messages are categorized into channels and subscribers receive only the channels they subscribed to. This provides stronger decoupling than the Observer pattern.
| 1 | // Pub/Sub implementation |
| 2 | class PubSub { |
| 3 | constructor() { |
| 4 | this._channels = new Map(); |
| 5 | } |
| 6 | |
| 7 | subscribe(channel, handler) { |
| 8 | if (!this._channels.has(channel)) { |
| 9 | this._channels.set(channel, new Set()); |
| 10 | } |
| 11 | this._channels.get(channel).add(handler); |
| 12 | |
| 13 | // Return unsubscribe function |
| 14 | return () => { |
| 15 | this._channels.get(channel)?.delete(handler); |
| 16 | }; |
| 17 | } |
| 18 | |
| 19 | publish(channel, data) { |
| 20 | const handlers = this._channels.get(channel); |
| 21 | if (!handlers) return; |
| 22 | |
| 23 | handlers.forEach(handler => { |
| 24 | try { |
| 25 | handler(data); |
| 26 | } catch (err) { |
| 27 | console.error(`Error in handler for ${channel}:`, err); |
| 28 | } |
| 29 | }); |
| 30 | } |
| 31 | |
| 32 | clear(channel) { |
| 33 | if (channel) { |
| 34 | this._channels.delete(channel); |
| 35 | } else { |
| 36 | this._channels.clear(); |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | count(channel) { |
| 41 | return this._channels.get(channel)?.size || 0; |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | // Application: notification system |
| 46 | const notifications = new PubSub(); |
| 47 | |
| 48 | const unsubOrder = notifications.subscribe('order:created', (order) => { |
| 49 | console.log(`New order #${order.id}: ${order.total}`); |
| 50 | sendConfirmationEmail(order); |
| 51 | }); |
| 52 | |
| 53 | notifications.subscribe('order:created', (order) => { |
| 54 | updateInventory(order.items); |
| 55 | }); |
| 56 | |
| 57 | notifications.subscribe('user:registered', (user) => { |
| 58 | console.log(`Welcome ${user.name}!`); |
| 59 | sendWelcomeEmail(user); |
| 60 | }); |
| 61 | |
| 62 | // Later |
| 63 | notifications.publish('order:created', { |
| 64 | id: 1234, |
| 65 | total: 99.99, |
| 66 | items: ['Widget', 'Gadget'] |
| 67 | }); |
| 68 | // Both subscribers for 'order:created' fire |
| 69 | |
| 70 | unsubOrder(); // Remove first subscriber |
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The strategy object can vary independently from the clients that use it. In JavaScript, strategies are often just functions passed as parameters, making the pattern nearly transparent.
| 1 | // Strategy pattern — validation example |
| 2 | const validators = { |
| 3 | required: (value) => value != null && value !== '', |
| 4 | email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), |
| 5 | minLength: (min) => (value) => value != null && value.length >= min, |
| 6 | maxLength: (max) => (value) => value == null || value.length <= max, |
| 7 | numeric: (value) => !isNaN(parseFloat(value)), |
| 8 | pattern: (regex) => (value) => regex.test(value), |
| 9 | }; |
| 10 | |
| 11 | function validate(value, rules) { |
| 12 | for (const rule of rules) { |
| 13 | const validator = typeof rule === 'function' |
| 14 | ? rule |
| 15 | : validators[rule.name]?.(...rule.args); |
| 16 | |
| 17 | if (!validator(value)) { |
| 18 | return { valid: false, error: rule.message || 'Validation failed' }; |
| 19 | } |
| 20 | } |
| 21 | return { valid: true }; |
| 22 | } |
| 23 | |
| 24 | // Usage |
| 25 | const rules = [ |
| 26 | { name: 'required', message: 'Email is required' }, |
| 27 | { name: 'email', message: 'Invalid email format' }, |
| 28 | { name: 'maxLength', args: [100], message: 'Email too long' }, |
| 29 | ]; |
| 30 | |
| 31 | console.log(validate('', rules)); // { valid: false, error: 'Email is required' } |
| 32 | console.log(validate('test', rules)); // { valid: false, error: 'Invalid email format' } |
| 33 | console.log(validate('a@b.com', rules)); // { valid: true } |
| 34 | |
| 35 | // Strategy pattern — sorting |
| 36 | const sortingStrategies = { |
| 37 | byName: (a, b) => a.name.localeCompare(b.name), |
| 38 | byPrice: (a, b) => a.price - b.price, |
| 39 | byRating: (a, b) => b.rating - a.rating, |
| 40 | byDate: (a, b) => new Date(b.date) - new Date(a.date), |
| 41 | }; |
| 42 | |
| 43 | function sortProducts(products, strategy) { |
| 44 | const fn = typeof strategy === 'string' |
| 45 | ? sortingStrategies[strategy] |
| 46 | : strategy; |
| 47 | return [...products].sort(fn); |
| 48 | } |
| 49 | |
| 50 | const products = [ |
| 51 | { name: 'Laptop', price: 999, rating: 4.5, date: '2024-01-15' }, |
| 52 | { name: 'Mouse', price: 25, rating: 4.2, date: '2024-03-01' }, |
| 53 | ]; |
| 54 | |
| 55 | console.log(sortProducts(products, 'byPrice')); |
| 56 | console.log(sortProducts(products, 'byRating')); |
Command Pattern
The Command pattern encapsulates a request as an object, allowing parameterization, queuing, logging, and undo/redo functionality. It separates the object that invokes the operation from the one that knows how to execute it. This is useful for task queues, transaction management, and editor undo systems.
| 1 | // Command pattern — undo/redo system |
| 2 | class Command { |
| 3 | constructor(execute, undo) { |
| 4 | this.execute = execute; |
| 5 | this.undo = undo; |
| 6 | } |
| 7 | } |
| 8 | |
| 9 | class CommandHistory { |
| 10 | constructor() { |
| 11 | this._history = []; |
| 12 | this._index = -1; |
| 13 | } |
| 14 | |
| 15 | execute(command) { |
| 16 | command.execute(); |
| 17 | this._history = this._history.slice(0, this._index + 1); |
| 18 | this._history.push(command); |
| 19 | this._index++; |
| 20 | } |
| 21 | |
| 22 | undo() { |
| 23 | if (this._index < 0) return; |
| 24 | this._history[this._index].undo(); |
| 25 | this._index--; |
| 26 | } |
| 27 | |
| 28 | redo() { |
| 29 | if (this._index >= this._history.length - 1) return; |
| 30 | this._index++; |
| 31 | this._history[this._index].execute(); |
| 32 | } |
| 33 | |
| 34 | clear() { |
| 35 | this._history = []; |
| 36 | this._index = -1; |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | // Application: text editor |
| 41 | class TextEditor { |
| 42 | constructor() { |
| 43 | this.content = ''; |
| 44 | this.history = new CommandHistory(); |
| 45 | } |
| 46 | |
| 47 | insert(text, position = this.content.length) { |
| 48 | return new Command( |
| 49 | () => { |
| 50 | this.content = this.content.slice(0, position) + text + this.content.slice(position); |
| 51 | }, |
| 52 | () => { |
| 53 | this.content = this.content.slice(0, position) + this.content.slice(position + text.length); |
| 54 | } |
| 55 | ); |
| 56 | } |
| 57 | |
| 58 | delete(start, end = start + 1) { |
| 59 | const deleted = this.content.slice(start, end); |
| 60 | return new Command( |
| 61 | () => { |
| 62 | this.content = this.content.slice(0, start) + this.content.slice(end); |
| 63 | }, |
| 64 | () => { |
| 65 | this.content = this.content.slice(0, start) + deleted + this.content.slice(start); |
| 66 | } |
| 67 | ); |
| 68 | } |
| 69 | |
| 70 | type(text) { |
| 71 | this.history.execute(this.insert(text)); |
| 72 | } |
| 73 | |
| 74 | backspace() { |
| 75 | if (this.content.length === 0) return; |
| 76 | this.history.execute(this.delete(this.content.length - 1)); |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | const editor = new TextEditor(); |
| 81 | editor.type('Hello'); |
| 82 | editor.type(' World'); |
| 83 | console.log(editor.content); // Hello World |
| 84 | editor.history.undo(); |
| 85 | console.log(editor.content); // Hello |
| 86 | editor.history.redo(); |
| 87 | console.log(editor.content); // Hello World |
best practice
Architectural patterns operate at a higher level than design patterns. They define the overall structure of an application, how components communicate, and where responsibilities lie. These patterns are not mutually exclusive — many modern applications blend elements from multiple architectural approaches.
MVC, MVP & MVVM
Model-View-Controller (MVC) and its variants (MVP, MVVM) are architectural patterns that separate data, presentation, and user interaction. They are foundational to frameworks like Angular (MVVM), Backbone.js (MVC), and modern component architectures.
| Pattern | Components | Data Flow | Best For |
|---|---|---|---|
| MVC | Model, View, Controller | Controller updates Model, View reads Model | Traditional web apps, server-rendered |
| MVP | Model, View, Presenter | View delegates to Presenter, Presenter updates Model | Complex UI, testability-critical |
| MVVM | Model, View, ViewModel | View binds to ViewModel, ViewModel exposes Model | Data-binding frameworks (Angular, Vue) |
| 1 | // Simple MVC example |
| 2 | // Model — pure data and business logic |
| 3 | class TodoModel { |
| 4 | constructor() { |
| 5 | this.todos = JSON.parse(localStorage.getItem('todos') || '[]'); |
| 6 | } |
| 7 | |
| 8 | add(text) { |
| 9 | this.todos.push({ id: Date.now(), text, completed: false }); |
| 10 | this._persist(); |
| 11 | } |
| 12 | |
| 13 | toggle(id) { |
| 14 | const todo = this.todos.find(t => t.id === id); |
| 15 | if (todo) todo.completed = !todo.completed; |
| 16 | this._persist(); |
| 17 | } |
| 18 | |
| 19 | remove(id) { |
| 20 | this.todos = this.todos.filter(t => t.id !== id); |
| 21 | this._persist(); |
| 22 | } |
| 23 | |
| 24 | _persist() { |
| 25 | localStorage.setItem('todos', JSON.stringify(this.todos)); |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | // View — renders UI |
| 30 | class TodoView { |
| 31 | constructor() { |
| 32 | this.list = document.getElementById('todoList'); |
| 33 | this.input = document.getElementById('todoInput'); |
| 34 | this.form = document.getElementById('todoForm'); |
| 35 | } |
| 36 | |
| 37 | render(todos) { |
| 38 | this.list.innerHTML = todos.map(todo => ` |
| 39 | <li data-id="${todo.id}" class="${todo.completed ? 'completed' : ''}"> |
| 40 | <input type="checkbox" ${todo.completed ? 'checked' : ''}> |
| 41 | <span>${todo.text}</span> |
| 42 | <button class="delete">×</button> |
| 43 | </li> |
| 44 | `).join(''); |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | // Controller — mediates between Model and View |
| 49 | class TodoController { |
| 50 | constructor(model, view) { |
| 51 | this.model = model; |
| 52 | this.view = view; |
| 53 | |
| 54 | this.view.form.addEventListener('submit', (e) => { |
| 55 | e.preventDefault(); |
| 56 | const text = this.view.input.value.trim(); |
| 57 | if (text) { |
| 58 | this.model.add(text); |
| 59 | this.view.input.value = ''; |
| 60 | this.view.render(this.model.todos); |
| 61 | } |
| 62 | }); |
| 63 | |
| 64 | this.view.list.addEventListener('click', (e) => { |
| 65 | if (e.target.classList.contains('delete')) { |
| 66 | const id = Number(e.target.closest('li').dataset.id); |
| 67 | this.model.remove(id); |
| 68 | this.view.render(this.model.todos); |
| 69 | } |
| 70 | }); |
| 71 | |
| 72 | // Initial render |
| 73 | this.view.render(this.model.todos); |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | const app = new TodoController(new TodoModel(), new TodoView()); |
note
Beyond the classic GoF patterns, JavaScript developers use patterns that leverage the language's unique features. These patterns are often more idiomatic and concise than their traditional counterparts.
Callback Pattern
The callback pattern is fundamental to JavaScript's asynchronous nature. A callback is a function passed as an argument to be invoked later, typically after an asynchronous operation completes. While largely superseded by Promises and async/await, callbacks remain important for event listeners, array methods, and Node.js APIs.
| 1 | // Callback pattern — event-driven |
| 2 | function fetchData(url, callback) { |
| 3 | const xhr = new XMLHttpRequest(); |
| 4 | xhr.open('GET', url); |
| 5 | xhr.onload = () => { |
| 6 | if (xhr.status === 200) { |
| 7 | callback(null, JSON.parse(xhr.responseText)); |
| 8 | } else { |
| 9 | callback(new Error(`Request failed: ${xhr.status}`)); |
| 10 | } |
| 11 | }; |
| 12 | xhr.onerror = () => callback(new Error('Network error')); |
| 13 | xhr.send(); |
| 14 | } |
| 15 | |
| 16 | // Error-first callback convention (Node.js style) |
| 17 | function readConfig(path, callback) { |
| 18 | fs.readFile(path, 'utf8', (err, data) => { |
| 19 | if (err) return callback(err); |
| 20 | try { |
| 21 | const config = JSON.parse(data); |
| 22 | callback(null, config); |
| 23 | } catch (parseErr) { |
| 24 | callback(parseErr); |
| 25 | } |
| 26 | }); |
| 27 | } |
| 28 | |
| 29 | // Callback with array methods |
| 30 | const numbers = [1, 2, 3, 4, 5]; |
| 31 | const doubled = numbers.map(n => n * 2); |
| 32 | const evens = numbers.filter(n => n % 2 === 0); |
| 33 | const sum = numbers.reduce((acc, n) => acc + n, 0); |
Promise Pattern
Promises represent a value that may be available now, later, or never. They provide a structured way to handle asynchronous operations with chaining, error propagation, and composition. The Promise pattern is the foundation of modern JavaScript async programming.
| 1 | // Promise pattern — async operation wrapper |
| 2 | function fetchUser(id) { |
| 3 | return new Promise((resolve, reject) => { |
| 4 | setTimeout(() => { |
| 5 | if (id <= 0) { |
| 6 | reject(new Error('Invalid user ID')); |
| 7 | return; |
| 8 | } |
| 9 | resolve({ id, name: 'Alice', role: 'admin' }); |
| 10 | }, 100); |
| 11 | }); |
| 12 | } |
| 13 | |
| 14 | // Promise chaining |
| 15 | fetchUser(1) |
| 16 | .then(user => { |
| 17 | console.log('User:', user); |
| 18 | return fetchUserPermissions(user.id); |
| 19 | }) |
| 20 | .then(permissions => { |
| 21 | console.log('Permissions:', permissions); |
| 22 | }) |
| 23 | .catch(error => { |
| 24 | console.error('Error:', error.message); |
| 25 | }) |
| 26 | .finally(() => { |
| 27 | console.log('Operation complete'); |
| 28 | }); |
| 29 | |
| 30 | // Promise composition |
| 31 | const users = Promise.all([ |
| 32 | fetchUser(1), |
| 33 | fetchUser(2), |
| 34 | fetchUser(3) |
| 35 | ]); |
| 36 | |
| 37 | const first = Promise.race([ |
| 38 | fetchUser(1), |
| 39 | fetchUser(2) |
| 40 | ]); |
| 41 | |
| 42 | // Async/await (syntactic sugar over promises) |
| 43 | async function loadDashboard() { |
| 44 | try { |
| 45 | const [user, posts, notifications] = await Promise.all([ |
| 46 | fetchUser(currentUserId), |
| 47 | fetchPosts(), |
| 48 | fetchNotifications() |
| 49 | ]); |
| 50 | return { user, posts, notifications }; |
| 51 | } catch (error) { |
| 52 | console.error('Dashboard load failed:', error); |
| 53 | throw error; |
| 54 | } |
| 55 | } |
Module Pattern (ES Modules)
ES Modules are the standardized module system for JavaScript. Each module has its own scope, and explicit import/export statements control what is shared. Modules are singletons — the same instance is shared across all imports. This is the modern replacement for IIFE-based module patterns.
| 1 | // math.js — named exports |
| 2 | export function add(a, b) { return a + b; } |
| 3 | export function subtract(a, b) { return a - b; } |
| 4 | export const PI = 3.14159; |
| 5 | |
| 6 | // utils.js — default export |
| 7 | export default function formatDate(date) { |
| 8 | return date.toISOString().split('T')[0]; |
| 9 | } |
| 10 | |
| 11 | // api.js — re-exporting |
| 12 | export { add, subtract } from './math.js'; |
| 13 | export { default as format } from './utils.js'; |
| 14 | |
| 15 | // app.js — importing |
| 16 | import { add, subtract, PI } from './math.js'; |
| 17 | import formatDate from './utils.js'; |
| 18 | import * as MathUtils from './math.js'; |
| 19 | |
| 20 | console.log(add(2, 3)); // 5 |
| 21 | console.log(MathUtils.PI); // 3.14159 |
| 22 | console.log(formatDate(new Date())); // 2026-07-07 |
| 23 | |
| 24 | // Dynamic imports (lazy loading) |
| 25 | const button = document.getElementById('loadBtn'); |
| 26 | button.addEventListener('click', async () => { |
| 27 | const { exportChart } = await import('./chart.js'); |
| 28 | exportChart(); |
| 29 | }); |
info
Design patterns are tools, not goals. Applying a pattern without understanding the problem leads to over-engineering and unnecessary complexity. Here is practical guidance on when — and when not — to use patterns.
| Situation | Recommended | Not Recommended |
|---|---|---|
| Small script / prototype | Simple functions, minimal structure | Full MVC/Observer/Command setup |
| Growing codebase | Module pattern, factory functions | Abstract Factory, complex hierarchies |
| Team project | Established patterns for consistency | Novel or obscure pattern combinations |
| Cross-cutting concerns | Decorator, Middleware | Inline duplication |
| State management | Observer, Pub/Sub, Redux pattern | Singleton with mutable global state |
Signs of Over-Engineering
best practice
pro tip