|$ curl https://forge-ai.dev/api/markdown?path=docs/js/design-patterns
$cat docs/javascript-—-design-patterns.md
updated This week·40 min read·published

JavaScript — Design Patterns

JavaScriptDesign PatternsAdvancedIntermediate to Advanced
Introduction

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

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.

module-pattern.js
JavaScript
1// Classic Module Pattern (IIFE)
2const 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
32console.log(CounterModule.getCount()); // 0
33CounterModule.increment();
34CounterModule.increment();
35console.log(CounterModule.getCount()); // 2
36// CounterModule.count — undefined (private)
37
38// Revealing Module Pattern
39const 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.

singleton.js
JavaScript
1// Singleton via module (ES modules are singletons by default)
2// config.js
3let instance = null;
4
5class 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
21export default new Config();
22
23// Simple singleton with module-scoped state
24let dbConnection = null;
25
26export 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
34class 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
58const logger1 = new Logger();
59const logger2 = new Logger();
60console.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.

factory.js
JavaScript
1// Simple factory function
2function 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
15const admin = createUser('admin', { name: 'Alice', id: 1 });
16const editor = createUser('editor', { name: 'Bob', id: 2 });
17
18// Factory with class hierarchy
19class Notification {
20 constructor(recipient, message) {
21 this.recipient = recipient;
22 this.message = message;
23 }
24 send() { throw new Error('Not implemented'); }
25}
26
27class EmailNotification extends Notification {
28 send() {
29 console.log(`Email to ${this.recipient}: ${this.message}`);
30 }
31}
32
33class SMSNotification extends Notification {
34 send() {
35 console.log(`SMS to ${this.recipient}: ${this.message}`);
36 }
37}
38
39class PushNotification extends Notification {
40 send() {
41 console.log(`Push to ${this.recipient}: ${this.message}`);
42 }
43}
44
45// Factory class
46class 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
57const notif = NotificationFactory.create('email', 'alice@example.com', 'Hello!');
58notif.send();

info

In JavaScript, the Factory pattern often simplifies to just a function. Unlike languages that require class hierarchies (Java's Factory Method), JavaScript's factory functions can return objects directly. This is idiomatic and reduces boilerplate. Use classes only when you need inheritance or instanceof checks.
Structural Patterns

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.

decorator.js
JavaScript
1// Function decorator pattern
2function 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
11function 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
21function 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
36function fetchUser(id) {
37 // Simulate expensive operation
38 const users = { 1: 'Alice', 2: 'Bob' };
39 return users[id] || null;
40}
41
42const cachedFetchUser = withCache(withLogging(fetchUser));
43console.log(cachedFetchUser(1)); // Logs call + cache miss
44console.log(cachedFetchUser(1)); // Logs call + cache hit
45
46// Class decorator (adding behavior to instances)
47function withTimestamp(Base) {
48 return class extends Base {
49 constructor(...args) {
50 super(...args);
51 this.createdAt = new Date();
52 }
53 };
54}
55
56@withTimestamp
57class 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.

mixin.js
JavaScript
1// Simple mixin via Object.assign
2const 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
11const 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
28class User {
29 constructor(name) {
30 this.name = name;
31 }
32}
33
34Object.assign(User.prototype, CanLog, CanEmit);
35
36const user = new User('Alice');
37user.log('User created'); // [User] User created
38user.on('login', () => console.log('Logged in!'));
39user.emit('login'); // Logged in!
40
41// Functional mixin pattern
42const withTimestamp = (Base) => class extends Base {
43 constructor(...args) {
44 super(...args);
45 this.createdAt = new Date();
46 }
47};
48
49const withSerializable = (Base) => class extends Base {
50 toJSON() {
51 return JSON.stringify({ ...this });
52 }
53};
54
55class Product {}
56const EnhancedProduct = withSerializable(withTimestamp(Product));
57const p = new EnhancedProduct();
58console.log(p.createdAt); // Date object
59console.log(p.toJSON()); // JSON string
🔥

pro tip

In JavaScript, the Decorator and Mixin patterns often overlap. The key difference: decorators wrap a single instance or function to add behavior, while mixins contribute a set of related methods to a class. Both favor composition over inheritance — a core principle of flexible software design.
Behavioral Patterns

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.

observer.js
JavaScript
1// Observer pattern implementation
2class 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
23class 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
34const newsChannel = new Subject();
35
36const alice = new Observer('Alice');
37const bob = new Observer('Bob');
38
39const unsubscribeAlice = newsChannel.subscribe(alice);
40newsChannel.subscribe(bob);
41
42newsChannel.notify({ headline: 'Breaking News!' });
43// Alice received: { headline: 'Breaking News!' }
44// Bob received: { headline: 'Breaking News!' }
45
46unsubscribeAlice();
47newsChannel.notify({ headline: 'Update 2' });
48// Only Bob receives the update
49
50// Observable with function observers (simpler)
51class 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
72const bus = new EventBus();
73bus.on('user:login', (user) => console.log(`${user.name} logged in`));
74bus.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.

pubsub.js
JavaScript
1// Pub/Sub implementation
2class 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
46const notifications = new PubSub();
47
48const unsubOrder = notifications.subscribe('order:created', (order) => {
49 console.log(`New order #${order.id}: ${order.total}`);
50 sendConfirmationEmail(order);
51});
52
53notifications.subscribe('order:created', (order) => {
54 updateInventory(order.items);
55});
56
57notifications.subscribe('user:registered', (user) => {
58 console.log(`Welcome ${user.name}!`);
59 sendWelcomeEmail(user);
60});
61
62// Later
63notifications.publish('order:created', {
64 id: 1234,
65 total: 99.99,
66 items: ['Widget', 'Gadget']
67});
68// Both subscribers for 'order:created' fire
69
70unsubOrder(); // 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.

strategy.js
JavaScript
1// Strategy pattern — validation example
2const 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
11function 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
25const 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
31console.log(validate('', rules)); // { valid: false, error: 'Email is required' }
32console.log(validate('test', rules)); // { valid: false, error: 'Invalid email format' }
33console.log(validate('a@b.com', rules)); // { valid: true }
34
35// Strategy pattern — sorting
36const 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
43function sortProducts(products, strategy) {
44 const fn = typeof strategy === 'string'
45 ? sortingStrategies[strategy]
46 : strategy;
47 return [...products].sort(fn);
48}
49
50const 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
55console.log(sortProducts(products, 'byPrice'));
56console.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.

command.js
JavaScript
1// Command pattern — undo/redo system
2class Command {
3 constructor(execute, undo) {
4 this.execute = execute;
5 this.undo = undo;
6 }
7}
8
9class 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
41class 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
80const editor = new TextEditor();
81editor.type('Hello');
82editor.type(' World');
83console.log(editor.content); // Hello World
84editor.history.undo();
85console.log(editor.content); // Hello
86editor.history.redo();
87console.log(editor.content); // Hello World

best practice

In JavaScript, patterns like Strategy, Command, and Observer are often simplified because functions are first-class. A Strategy can be a single function. A Command can be a function with closure state. An Observer can be a callback. Use this flexibility to reduce boilerplate, but be careful — overly implicit patterns can make code harder to understand and debug.
Architectural Patterns

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.

PatternComponentsData FlowBest For
MVCModel, View, ControllerController updates Model, View reads ModelTraditional web apps, server-rendered
MVPModel, View, PresenterView delegates to Presenter, Presenter updates ModelComplex UI, testability-critical
MVVMModel, View, ViewModelView binds to ViewModel, ViewModel exposes ModelData-binding frameworks (Angular, Vue)
mvc.js
JavaScript
1// Simple MVC example
2// Model — pure data and business logic
3class 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
30class 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
49class 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
77const app = new TodoController(new TodoModel(), new TodoView());
📝

note

Modern frontend frameworks (React, Vue, Svelte) have largely replaced explicit MVC/MVVM with component-based architectures. Components encapsulate both presentation and behavior, blurring the traditional boundaries. However, the underlying principle remains: separate concerns to manage complexity. Even in React, you often have data layer (Model), components (View), and event handlers/effects (Controller).
JavaScript-Specific Patterns

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.

callback-pattern.js
JavaScript
1// Callback pattern — event-driven
2function 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)
17function 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
30const numbers = [1, 2, 3, 4, 5];
31const doubled = numbers.map(n => n * 2);
32const evens = numbers.filter(n => n % 2 === 0);
33const 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.

promise-pattern.js
JavaScript
1// Promise pattern — async operation wrapper
2function 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
15fetchUser(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
31const users = Promise.all([
32 fetchUser(1),
33 fetchUser(2),
34 fetchUser(3)
35]);
36
37const first = Promise.race([
38 fetchUser(1),
39 fetchUser(2)
40]);
41
42// Async/await (syntactic sugar over promises)
43async 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.

es-modules.js
JavaScript
1// math.js — named exports
2export function add(a, b) { return a + b; }
3export function subtract(a, b) { return a - b; }
4export const PI = 3.14159;
5
6// utils.js — default export
7export default function formatDate(date) {
8 return date.toISOString().split('T')[0];
9}
10
11// api.js — re-exporting
12export { add, subtract } from './math.js';
13export { default as format } from './utils.js';
14
15// app.js — importing
16import { add, subtract, PI } from './math.js';
17import formatDate from './utils.js';
18import * as MathUtils from './math.js';
19
20console.log(add(2, 3)); // 5
21console.log(MathUtils.PI); // 3.14159
22console.log(formatDate(new Date())); // 2026-07-07
23
24// Dynamic imports (lazy loading)
25const button = document.getElementById('loadBtn');
26button.addEventListener('click', async () => {
27 const { exportChart } = await import('./chart.js');
28 exportChart();
29});

info

ES Modules have replaced the IIFE module pattern and CommonJS (require) in modern JavaScript. They provide static analysis (enabling tree shaking), cyclic dependency handling, and native browser support. Always prefer ES Modules for new projects. Use dynamic import() for lazy loading and code splitting.
When to Use Patterns

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.

SituationRecommendedNot Recommended
Small script / prototypeSimple functions, minimal structureFull MVC/Observer/Command setup
Growing codebaseModule pattern, factory functionsAbstract Factory, complex hierarchies
Team projectEstablished patterns for consistencyNovel or obscure pattern combinations
Cross-cutting concernsDecorator, MiddlewareInline duplication
State managementObserver, Pub/Sub, Redux patternSingleton with mutable global state

Signs of Over-Engineering

Adding a pattern for hypothetical future requirements that may never come
Using Abstract Factory when a simple factory function or direct constructor call suffices
Building a full Pub/Sub system when props/callbacks between two components are enough
Implementing Command pattern with undo stack for operations that never need undo
Class hierarchies that exist only to conform to a pattern, not to solve a real problem
Patterns that make the code harder to follow than the direct approach
Refactoring working code to use patterns without any concrete benefit

best practice

The best pattern is often the simplest thing that works. Start with plain functions and objects. Introduce patterns only when you feel concrete pain — repeated code that is hard to maintain, coupling that makes testing difficult, or changes that ripple through too many files. Patterns should make your code simpler to change, not add complexity for its own sake.
Best Practices
Prefer composition over inheritance — use mixins, higher-order functions, and factory functions instead of deeply nested class hierarchies
Use the Module pattern (ES modules) by default — they provide encapsulation, explicit interfaces, and tree-shaking
Leverage JavaScript's first-class functions — a Strategy is often just a function, not a class hierarchy
Apply patterns incrementally — do not design the full architecture upfront; refactor into patterns as needs emerge
Keep patterns consistent within a project — mixing Observer, Pub/Sub, and signals in the same codebase creates confusion
Test pattern implementations in isolation — patterns add abstraction, and abstraction should be tested
Document why a pattern was chosen — the next developer needs to understand the problem it solves, not just see the structure
Remember that patterns are language-dependent — JavaScript's dynamic nature makes many GoF patterns trivial (Strategy, Command, Factory)
🔥

pro tip

Read the original Design Patterns book (Gamma, Helm, Johnson, Vlissides — the "Gang of Four") but interpret it through a JavaScript lens. Many patterns in the book were designed for statically-typed languages like C++. In JavaScript, first-class functions, closures, dynamic objects, and prototypal inheritance make many patterns simpler or even invisible. The value is in the problem-solving mindset, not the specific class diagrams.
$Blueprint — Engineering Documentation·Section ID: JS-DESIGN·Revision: 1.0