Creational Factory Singleton Structural Module Facade Behavioral Observer Strategy Design Patterns Create Objects Compose Communicate
Frontend

JavaScript Design Patterns Every Developer Should Know

Mayur Dabhi
Mayur Dabhi
April 4, 2026
22 min read

Design patterns are reusable solutions to commonly occurring problems in software design. They represent best practices evolved over time by experienced software developers. In JavaScript, understanding design patterns is crucial because the language's flexibility allows for multiple ways to solve the same problem—and not all of them are equally maintainable or scalable.

Whether you're building a small utility library or architecting a large-scale application, design patterns provide a shared vocabulary and proven approaches that make your code more readable, testable, and maintainable. In this comprehensive guide, we'll explore the essential design patterns every JavaScript developer should master, with practical examples and real-world use cases.

Why Learn Design Patterns?
  • Reusable Solutions: Apply proven approaches to common problems
  • Better Communication: Share ideas using a common vocabulary
  • Improved Code Quality: Write more maintainable and testable code
  • Framework Understanding: Recognize patterns in libraries like React, Vue, and Angular
  • Interview Preparation: Design patterns are frequently asked in technical interviews

Understanding Pattern Categories

Design patterns are traditionally grouped into three categories, each addressing different aspects of software design. Understanding these categories helps you identify which pattern might be most appropriate for your specific problem.

Creational "How objects are created" • Singleton • Factory • Abstract Factory • Builder • Prototype Structural "How objects are composed" • Module • Facade • Decorator • Adapter • Proxy Behavioral "How objects communicate" • Observer • Strategy • Command • Iterator • Mediator

The three main categories of design patterns and their primary concerns

1. The Singleton Pattern

Creational Pattern

The Singleton pattern ensures a class has only one instance and provides a global point of access to it. This is particularly useful when exactly one object is needed to coordinate actions across the system—like a configuration manager, logging service, or database connection pool.

Request 1 Request 2 Request 3 Singleton getInstance() Single Instance Same Object Reference

Multiple requests always receive the same singleton instance

singleton.js
class DatabaseConnection {
  static instance = null;
  
  constructor() {
    if (DatabaseConnection.instance) {
      return DatabaseConnection.instance;
    }
    
    this.connection = this.createConnection();
    DatabaseConnection.instance = this;
  }
  
  createConnection() {
    // Simulate database connection
    console.log('Creating new database connection...');
    return {
      host: 'localhost',
      port: 5432,
      connected: true
    };
  }
  
  query(sql) {
    console.log(`Executing: ${sql}`);
    return { rows: [], affected: 0 };
  }
  
  static getInstance() {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }
}

// Usage - both variables reference the SAME instance
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();

console.log(db1 === db2); // true - same instance!
When to Use Singleton

Use Singleton sparingly! While useful for truly global resources (logging, caching, configuration), overuse can lead to hidden dependencies and make testing difficult. Consider dependency injection as an alternative for better testability.

2. The Factory Pattern

Creational Pattern

The Factory pattern provides an interface for creating objects without specifying their exact classes. It's incredibly useful when you need to create different types of objects based on conditions, or when the creation logic is complex and should be centralized.

// Simple Factory for creating UI components
class Button {
  constructor(text) {
    this.text = text;
    this.type = 'button';
  }
  render() {
    return `<button>${this.text}</button>`;
  }
}

class Input {
  constructor(placeholder) {
    this.placeholder = placeholder;
    this.type = 'input';
  }
  render() {
    return `<input placeholder="${this.placeholder}" />`;
  }
}

class Select {
  constructor(options) {
    this.options = options;
    this.type = 'select';
  }
  render() {
    const opts = this.options.map(o => `<option>${o}</option>`).join('');
    return `<select>${opts}</select>`;
  }
}

// The Factory
class UIFactory {
  static create(type, config) {
    switch (type) {
      case 'button':
        return new Button(config.text);
      case 'input':
        return new Input(config.placeholder);
      case 'select':
        return new Select(config.options);
      default:
        throw new Error(`Unknown component: ${type}`);
    }
  }
}

// Usage
const submitBtn = UIFactory.create('button', { text: 'Submit' });
const emailInput = UIFactory.create('input', { placeholder: 'Enter email' });
// Factory Method Pattern - Let subclasses decide
class NotificationCreator {
  // Factory Method - to be overridden
  createNotification() {
    throw new Error('Implement createNotification()');
  }
  
  send(message) {
    const notification = this.createNotification();
    notification.format(message);
    notification.deliver();
  }
}

class EmailNotificationCreator extends NotificationCreator {
  createNotification() {
    return new EmailNotification();
  }
}

class SMSNotificationCreator extends NotificationCreator {
  createNotification() {
    return new SMSNotification();
  }
}

class PushNotificationCreator extends NotificationCreator {
  createNotification() {
    return new PushNotification();
  }
}

// Usage - the creator decides the type
function notifyUser(userPreference, message) {
  let creator;
  switch (userPreference) {
    case 'email': creator = new EmailNotificationCreator(); break;
    case 'sms': creator = new SMSNotificationCreator(); break;
    case 'push': creator = new PushNotificationCreator(); break;
  }
  creator.send(message);
}

Real-World Examples of Factory Pattern:

  • React.createElement() - Creates React elements based on type
  • document.createElement() - DOM element factory
  • jQuery $() - Creates jQuery objects from selectors
  • Express middleware - Factories that return handler functions
// Real-world: API Response Factory
class ApiResponseFactory {
  static success(data, message = 'Success') {
    return {
      success: true,
      message,
      data,
      timestamp: new Date().toISOString()
    };
  }
  
  static error(error, code = 500) {
    return {
      success: false,
      error: error.message || error,
      code,
      timestamp: new Date().toISOString()
    };
  }
  
  static paginated(data, page, total, perPage) {
    return {
      success: true,
      data,
      pagination: {
        currentPage: page,
        totalPages: Math.ceil(total / perPage),
        totalItems: total,
        perPage
      }
    };
  }
}

3. The Module Pattern

Structural Pattern

The Module pattern is one of the most important patterns in JavaScript. It provides a way to encapsulate private members while exposing a public API. Before ES6 modules, this was the primary way to organize code and avoid polluting the global namespace.

Module Private _privateVar _helper() _cache Public API init() getData() update() Accessible Hidden

The Module pattern encapsulates private members and exposes only the public API

module-pattern.js
// Classic Module Pattern (IIFE)
const ShoppingCart = (function() {
  // Private variables
  let items = [];
  let total = 0;
  
  // Private methods
  function calculateTotal() {
    total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    return total;
  }
  
  function findItem(id) {
    return items.find(item => item.id === id);
  }
  
  // Public API
  return {
    addItem(product, quantity = 1) {
      const existing = findItem(product.id);
      if (existing) {
        existing.quantity += quantity;
      } else {
        items.push({ ...product, quantity });
      }
      return calculateTotal();
    },
    
    removeItem(id) {
      items = items.filter(item => item.id !== id);
      return calculateTotal();
    },
    
    getTotal() {
      return total;
    },
    
    getItems() {
      // Return a copy to prevent external mutation
      return [...items];
    },
    
    clear() {
      items = [];
      total = 0;
    }
  };
})();

// Usage
ShoppingCart.addItem({ id: 1, name: 'Laptop', price: 999 });
ShoppingCart.addItem({ id: 2, name: 'Mouse', price: 25 }, 2);

console.log(ShoppingCart.getTotal());  // 1049
console.log(ShoppingCart.getItems()); // [...]

// Private members are inaccessible
console.log(ShoppingCart.items);     // undefined
console.log(ShoppingCart.calculateTotal); // undefined

Encapsulation

Hide internal implementation details and protect data from external modification.

Namespace

Avoid global scope pollution by containing all functionality within a single object.

Organization

Group related functionality together, making code easier to maintain and understand.

Reusability

Create self-contained units that can be easily reused across projects.

4. The Observer Pattern

Behavioral Pattern

The Observer pattern defines a one-to-many dependency between objects. When the subject (observable) changes state, all its dependents (observers) are notified and updated automatically. This pattern is fundamental to event-driven programming and is extensively used in frameworks like React, Vue, and RxJS.

Subject (Observable) subscribe() notify() Observer 1 UI Component update() Observer 2 Logger update() Observer 3 Analytics update()

The Subject notifies all subscribed Observers when state changes

observer.js
// EventEmitter - A flexible Observer implementation
class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  // Subscribe to an event
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    
    // Return unsubscribe function
    return () => this.off(event, callback);
  }
  
  // Subscribe once
  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(event, wrapper);
    };
    return this.on(event, wrapper);
  }
  
  // Unsubscribe
  off(event, callback) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event]
      .filter(cb => cb !== callback);
  }
  
  // Emit event to all subscribers
  emit(event, ...args) {
    if (!this.events[event]) return;
    this.events[event].forEach(callback => {
      callback(...args);
    });
  }
}

// Usage: User Authentication State
class AuthService extends EventEmitter {
  constructor() {
    super();
    this.user = null;
  }
  
  login(userData) {
    this.user = userData;
    this.emit('login', userData);
    this.emit('authStateChange', { isAuthenticated: true, user: userData });
  }
  
  logout() {
    const previousUser = this.user;
    this.user = null;
    this.emit('logout', previousUser);
    this.emit('authStateChange', { isAuthenticated: false, user: null });
  }
}

// Multiple observers listening to auth events
const auth = new AuthService();

// UI updates
auth.on('authStateChange', ({ isAuthenticated, user }) => {
  updateNavbar(isAuthenticated, user);
});

// Analytics tracking
auth.on('login', (user) => {
  analytics.track('user_login', { userId: user.id });
});

// Session management
auth.on('logout', () => {
  localStorage.removeItem('session');
});
Observer Pattern in Modern JavaScript

The Observer pattern is everywhere in JavaScript:

  • DOM Events: element.addEventListener('click', handler)
  • React/Vue: State changes trigger component re-renders
  • RxJS: Observables and subscriptions
  • Node.js: EventEmitter in core modules
  • Redux: Store subscribers are notified on state changes

5. The Strategy Pattern

Behavioral Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern lets the algorithm vary independently from the clients that use it. It's perfect for situations where you need different variations of an algorithm or behavior.

strategy.js
// Payment Strategy Pattern

// Strategy Interface (implicitly defined in JS)
class PaymentStrategy {
  pay(amount) {
    throw new Error('pay() must be implemented');
  }
  validate() {
    throw new Error('validate() must be implemented');
  }
}

// Concrete Strategies
class CreditCardPayment extends PaymentStrategy {
  constructor(cardNumber, cvv, expiry) {
    super();
    this.cardNumber = cardNumber;
    this.cvv = cvv;
    this.expiry = expiry;
  }
  
  validate() {
    return this.cardNumber.length === 16 && this.cvv.length === 3;
  }
  
  pay(amount) {
    console.log(`Paid $${amount} using Credit Card ending in ${this.cardNumber.slice(-4)}`);
    return { success: true, method: 'credit_card' };
  }
}

class PayPalPayment extends PaymentStrategy {
  constructor(email) {
    super();
    this.email = email;
  }
  
  validate() {
    return this.email.includes('@');
  }
  
  pay(amount) {
    console.log(`Paid $${amount} using PayPal account ${this.email}`);
    return { success: true, method: 'paypal' };
  }
}

class CryptoPayment extends PaymentStrategy {
  constructor(walletAddress, currency = 'BTC') {
    super();
    this.walletAddress = walletAddress;
    this.currency = currency;
  }
  
  validate() {
    return this.walletAddress.length >= 26;
  }
  
  pay(amount) {
    console.log(`Paid $${amount} in ${this.currency} to wallet ${this.walletAddress.slice(0, 8)}...`);
    return { success: true, method: 'crypto', currency: this.currency };
  }
}

// Context - uses the strategy
class PaymentProcessor {
  constructor() {
    this.strategy = null;
  }
  
  setStrategy(strategy) {
    this.strategy = strategy;
  }
  
  checkout(amount) {
    if (!this.strategy) {
      throw new Error('Please select a payment method');
    }
    
    if (!this.strategy.validate()) {
      throw new Error('Invalid payment details');
    }
    
    return this.strategy.pay(amount);
  }
}

// Usage - strategy can be swapped at runtime
const processor = new PaymentProcessor();

// User selects credit card
processor.setStrategy(new CreditCardPayment('4111111111111111', '123', '12/25'));
processor.checkout(99.99);

// Later, user switches to PayPal
processor.setStrategy(new PayPalPayment('user@email.com'));
processor.checkout(49.99);

6. The Decorator Pattern

Structural Pattern

The Decorator pattern allows you to add new behavior to objects dynamically by wrapping them in decorator objects. This provides a flexible alternative to subclassing for extending functionality. In JavaScript, this pattern is commonly used with higher-order functions and is the foundation of many middleware systems.

decorator.js
// Function Decorator Pattern

// Base function
function fetchData(url) {
  return fetch(url).then(res => res.json());
}

// Decorator: Add logging
function withLogging(fn) {
  return async function(...args) {
    console.log(`Calling ${fn.name} with:`, args);
    const start = Date.now();
    
    try {
      const result = await fn(...args);
      console.log(`${fn.name} completed in ${Date.now() - start}ms`);
      return result;
    } catch (error) {
      console.error(`${fn.name} failed:`, error);
      throw error;
    }
  };
}

// Decorator: Add caching
function withCache(fn, ttl = 60000) {
  const cache = new Map();
  
  return async function(...args) {
    const key = JSON.stringify(args);
    const cached = cache.get(key);
    
    if (cached && Date.now() - cached.timestamp < ttl) {
      console.log('Cache hit!');
      return cached.data;
    }
    
    const result = await fn(...args);
    cache.set(key, { data: result, timestamp: Date.now() });
    return result;
  };
}

// Decorator: Add retry logic
function withRetry(fn, maxRetries = 3) {
  return async function(...args) {
    let lastError;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await fn(...args);
      } catch (error) {
        lastError = error;
        console.log(`Attempt ${attempt} failed, retrying...`);
        await new Promise(r => setTimeout(r, 1000 * attempt));
      }
    }
    
    throw lastError;
  };
}

// Compose decorators - order matters!
const enhancedFetch = withLogging(
  withCache(
    withRetry(fetchData)
  )
);

// Usage - now has logging, caching, and retry!
enhancedFetch('/api/users').then(data => console.log(data));

7. The Proxy Pattern

Structural Pattern

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. JavaScript has built-in support for proxies via the Proxy object, making this pattern particularly powerful for validation, logging, lazy loading, and more.

proxy.js
// Validation Proxy
function createValidatedObject(target, schema) {
  return new Proxy(target, {
    set(obj, prop, value) {
      const validator = schema[prop];
      
      if (validator) {
        const { valid, message } = validator(value);
        if (!valid) {
          throw new Error(`Invalid ${prop}: ${message}`);
        }
      }
      
      obj[prop] = value;
      return true;
    }
  });
}

// Define validation schema
const userSchema = {
  age(value) {
    if (typeof value !== 'number') {
      return { valid: false, message: 'must be a number' };
    }
    if (value < 0 || value > 150) {
      return { valid: false, message: 'must be between 0 and 150' };
    }
    return { valid: true };
  },
  email(value) {
    if (!value.includes('@')) {
      return { valid: false, message: 'must be valid email' };
    }
    return { valid: true };
  }
};

// Usage
const user = createValidatedObject({}, userSchema);
user.name = 'John';     // OK - no validator
user.age = 25;         // OK - valid age
user.email = 'john@example.com'; // OK - valid email
// user.age = -5;       // Error: Invalid age: must be between 0 and 150
// user.email = 'invalid'; // Error: Invalid email: must be valid email

// Lazy Loading Proxy (Virtual Proxy)
function createLazyLoader(expensiveInit) {
  let instance = null;
  
  return new Proxy({}, {
    get(target, prop) {
      if (!instance) {
        console.log('Initializing expensive resource...');
        instance = expensiveInit();
      }
      return instance[prop];
    }
  });
}

const heavyResource = createLazyLoader(() => {
  // Expensive initialization
  return { data: 'expensive data', process() { return 'processed'; } };
});

// Resource not initialized yet!
console.log('Resource created but not initialized');

// First access triggers initialization
console.log(heavyResource.data); // "Initializing..." then "expensive data"

Pattern Comparison: When to Use What

Pattern Category Use Case Example
Singleton Creational Single shared instance Configuration, Logging
Factory Creational Object creation logic UI Components, API Responses
Module Structural Encapsulation & namespacing Libraries, Services
Observer Behavioral Event-driven updates State Management, Events
Strategy Behavioral Interchangeable algorithms Payment, Sorting, Validation
Decorator Structural Add behavior dynamically Middleware, HOCs
Proxy Structural Control access to objects Validation, Lazy Loading

Best Practices and Guidelines

When to Use Design Patterns

  • Don't force it: Use patterns when they solve a real problem, not just because they exist
  • Start simple: Begin with the simplest solution; refactor to patterns when complexity demands it
  • Understand the problem: Make sure you understand why a pattern exists before applying it
  • Consider maintenance: Patterns should make code easier to maintain, not harder
  • Document your choices: When using patterns, document why you chose them

Common Anti-Patterns to Avoid

  • Singleton Abuse: Using Singleton when dependency injection would be better
  • Over-engineering: Applying patterns to simple problems that don't need them
  • Pattern Mania: Using multiple patterns when one would suffice
  • Ignoring Built-ins: Creating custom patterns when JavaScript/frameworks provide solutions
  • Tight Coupling: Implementing patterns in ways that increase coupling instead of reducing it

Conclusion

Design patterns are powerful tools in your JavaScript toolkit, but they're not silver bullets. The key is understanding when and why to use each pattern, not just how to implement them. As you gain experience, you'll develop an intuition for recognizing situations where patterns apply.

Remember these core principles:

Key Takeaways

  • Creational patterns handle object creation - use when instantiation logic is complex
  • Structural patterns deal with composition - use to simplify relationships between objects
  • Behavioral patterns manage algorithms and communication - use for complex workflows
  • Modern JavaScript features (classes, modules, Proxy) make patterns cleaner to implement
  • Many frameworks (React, Vue, Express) are built on these patterns - understanding them helps you use frameworks better

Start by mastering the patterns covered in this guide: Singleton, Factory, Module, Observer, Strategy, Decorator, and Proxy. These seven patterns will address the majority of architectural challenges you'll encounter in JavaScript development. As you grow, explore additional patterns like Command, State, and Mediator to expand your architectural vocabulary.

JavaScript Design Patterns Best Practices Software Architecture Clean Code
Mayur Dabhi

Mayur Dabhi

Full Stack Developer passionate about clean code, design patterns, and building scalable applications. I write about web development best practices and modern JavaScript techniques.