Software Engineering

Design Patterns in JavaScript: Essential Patterns for Modern Development

Learn key design patterns in JavaScript including Singleton, Factory, Observer, Strategy, and more with real-world examples and modern implementations.

A

Amr S.

Author & Developer

20 min read
October 17, 2025
999+ views
Design Patterns in JavaScript: Essential Patterns for Modern Development

Design Patterns in JavaScript: Essential Patterns for Modern Development

Design patterns are reusable solutions to commonly occurring problems in software design. They represent best practices that have evolved over time and provide a common vocabulary for developers to communicate complex design concepts efficiently.

Creational Patterns

Singleton Pattern

javascript
class ConfigManager {
  static #instance = null;
  #config = {};

  constructor() {
    if (ConfigManager.#instance) {
      return ConfigManager.#instance;
    }
    ConfigManager.#instance = this;
  }

  set(key, value) {
    this.#config[key] = value;
  }

  get(key) {
    return this.#config[key];
  }

  static getInstance() {
    if (!ConfigManager.#instance) {
      ConfigManager.#instance = new ConfigManager();
    }
    return ConfigManager.#instance;
  }
}

// Usage
const config1 = new ConfigManager();
const config2 = ConfigManager.getInstance();
console.log(config1 === config2); // true

Factory Pattern

javascript
class PaymentFactory {
  static createPayment(type, amount) {
    switch (type) {
      case 'credit-card':
        return new CreditCardPayment(amount);
      case 'paypal':
        return new PayPalPayment(amount);
      case 'crypto':
        return new CryptoPayment(amount);
      default:
        throw new Error(`Payment type ${type} not supported`);
    }
  }
}

class CreditCardPayment {
  constructor(amount) { this.amount = amount; }
  process() { console.log(`Processing $${this.amount} via Credit Card`); }
}

class PayPalPayment {
  constructor(amount) { this.amount = amount; }
  process() { console.log(`Processing $${this.amount} via PayPal`); }
}

// Usage
const payment = PaymentFactory.createPayment('credit-card', 100);
payment.process();

Structural Patterns

Adapter Pattern

javascript
// Legacy API
class LegacyPrinter {
  printOldFormat(text) {
    console.log(`Legacy: ${text}`);
  }
}

// Modern interface
class ModernPrinter {
  print(content) {
    console.log(`Modern: ${content}`);
  }
}

// Adapter to make legacy work with modern interface
class PrinterAdapter {
  constructor(legacyPrinter) {
    this.legacyPrinter = legacyPrinter;
  }

  print(content) {
    this.legacyPrinter.printOldFormat(content);
  }
}

// Usage
const legacy = new LegacyPrinter();
const adapter = new PrinterAdapter(legacy);
adapter.print('Hello World'); // Works with modern interface

Decorator Pattern

javascript
class Coffee {
  cost() { return 5; }
  description() { return 'Simple coffee'; }
}

class CoffeeDecorator {
  constructor(coffee) { this.coffee = coffee; }
  cost() { return this.coffee.cost(); }
  description() { return this.coffee.description(); }
}

class MilkDecorator extends CoffeeDecorator {
  cost() { return this.coffee.cost() + 2; }
  description() { return this.coffee.description() + ', milk'; }
}

class SugarDecorator extends CoffeeDecorator {
  cost() { return this.coffee.cost() + 1; }
  description() { return this.coffee.description() + ', sugar'; }
}

// Usage
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(`${coffee.description()} - $${coffee.cost()}`);

Behavioral Patterns

Observer Pattern

javascript
class EventEmitter {
  constructor() { this.listeners = {}; }

  on(event, callback) {
    if (!this.listeners[event]) this.listeners[event] = [];
    this.listeners[event].push(callback);
  }

  emit(event, data) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(callback => callback(data));
    }
  }

  off(event, callback) {
    if (this.listeners[event]) {
      this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
    }
  }
}

// Usage
const emitter = new EventEmitter();
emitter.on('userLogin', user => console.log(`User ${user.name} logged in`));
emitter.emit('userLogin', { name: 'John' });

Strategy Pattern

javascript
class PaymentContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  pay(amount) {
    return this.strategy.pay(amount);
  }
}

class CreditCardStrategy {
  pay(amount) {
    console.log(`Paid $${amount} using Credit Card`);
  }
}

class PayPalStrategy {
  pay(amount) {
    console.log(`Paid $${amount} using PayPal`);
  }
}

// Usage
const payment = new PaymentContext(new CreditCardStrategy());
payment.pay(100);
payment.setStrategy(new PayPalStrategy());
payment.pay(50);

Command Pattern

javascript
class TextEditor {
  constructor() {
    this.content = '';
    this.history = [];
  }

  write(text) {
    this.content += text;
  }

  delete(length) {
    this.content = this.content.slice(0, -length);
  }

  save() {
    this.history.push(this.content);
  }

  restore() {
    this.content = this.history.pop() || '';
  }
}

class Command {
  execute() { throw new Error('Execute method must be implemented'); }
  undo() { throw new Error('Undo method must be implemented'); }
}

class WriteCommand extends Command {
  constructor(editor, text) {
    super();
    this.editor = editor;
    this.text = text;
  }

  execute() {
    this.editor.write(this.text);
  }

  undo() {
    this.editor.delete(this.text.length);
  }
}

// Usage
const editor = new TextEditor();
const writeHello = new WriteCommand(editor, 'Hello ');
const writeWorld = new WriteCommand(editor, 'World!');

writeHello.execute();
writeWorld.execute();
console.log(editor.content); // "Hello World!"

writeWorld.undo();
console.log(editor.content); // "Hello "

Modern JavaScript Patterns

Module Pattern

javascript
// ES6 Module Pattern
const UserModule = (() => {
  let users = [];

  const addUser = (user) => {
    users.push(user);
  };

  const getUsers = () => [...users];

  const findUser = (id) => users.find(user => user.id === id);

  return {
    addUser,
    getUsers,
    findUser
  };
})();

// Usage
UserModule.addUser({ id: 1, name: 'John' });
console.log(UserModule.getUsers());

Pub/Sub Pattern

javascript
class PubSub {
  constructor() {
    this.subscribers = {};
  }

  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push(callback);

    return () => {
      this.subscribers[event] = this.subscribers[event].filter(cb => cb !== callback);
    };
  }

  publish(event, data) {
    if (this.subscribers[event]) {
      this.subscribers[event].forEach(callback => {
        try {
          callback(data);
        } catch (error) {
          console.error('Error in subscriber:', error);
        }
      });
    }
  }
}

// Global instance
const pubsub = new PubSub();

// Usage
const unsubscribe = pubsub.subscribe('userCreated', (user) => {
  console.log('New user created:', user.name);
});

pubsub.publish('userCreated', { name: 'Alice' });
unsubscribe(); // Clean up

Real-World Example: Shopping Cart System

javascript
// Combining multiple patterns in a shopping cart system

// Singleton for cart state
class CartManager {
  static #instance = null;
  
  constructor() {
    if (CartManager.#instance) return CartManager.#instance;
    this.items = [];
    this.observers = [];
    CartManager.#instance = this;
  }

  addItem(item) {
    this.items.push(item);
    this.notifyObservers('itemAdded', item);
  }

  subscribe(callback) {
    this.observers.push(callback);
  }

  notifyObservers(event, data) {
    this.observers.forEach(callback => callback(event, data));
  }

  static getInstance() {
    return CartManager.#instance || new CartManager();
  }
}

// Strategy for different discount types
class DiscountStrategy {
  calculate(amount) { return amount; }
}

class PercentageDiscount extends DiscountStrategy {
  constructor(percentage) {
    super();
    this.percentage = percentage;
  }
  calculate(amount) {
    return amount * (1 - this.percentage / 100);
  }
}

class FixedDiscount extends DiscountStrategy {
  constructor(amount) {
    super();
    this.discountAmount = amount;
  }
  calculate(amount) {
    return Math.max(0, amount - this.discountAmount);
  }
}

// Factory for creating different payment methods
class PaymentFactory {
  static createPayment(type, config) {
    switch (type) {
      case 'stripe':
        return new StripePayment(config);
      case 'paypal':
        return new PayPalPayment(config);
      default:
        throw new Error(`Payment type ${type} not supported`);
    }
  }
}

class StripePayment {
  constructor(config) { this.config = config; }
  process(amount) {
    console.log(`Processing $${amount} via Stripe`);
    return { success: true, transactionId: 'stripe_' + Date.now() };
  }
}

// Command pattern for cart operations
class AddItemCommand {
  constructor(cart, item) {
    this.cart = cart;
    this.item = item;
  }

  execute() {
    this.cart.addItem(this.item);
  }

  undo() {
    const index = this.cart.items.findIndex(i => i.id === this.item.id);
    if (index > -1) this.cart.items.splice(index, 1);
  }
}

// Usage
const cart = CartManager.getInstance();
cart.subscribe((event, data) => {
  console.log(`Cart event: ${event}`, data);
});

const addCommand = new AddItemCommand(cart, { id: 1, name: 'Laptop', price: 999 });
addCommand.execute();

const discount = new PercentageDiscount(10);
const finalPrice = discount.calculate(999);
console.log(`Final price: $${finalPrice}`);

When to Use Each Pattern

  • Singleton: Global configuration, logging, caching systems
  • Factory: Creating objects based on user input or configuration
  • Observer: Event systems, model-view architectures, real-time updates
  • Strategy: Payment processing, sorting algorithms, validation rules
  • Command: Undo/redo functionality, queuing operations, macro recording
  • Decorator: Adding features to objects dynamically, middleware systems
  • Adapter: Integrating third-party libraries, legacy code integration

Best Practices

💡 Design patterns are tools, not rules. Use them when they solve a real problem, not just because they exist.

  • Start simple and refactor to patterns when complexity justifies it
  • Focus on readability and maintainability over clever implementations
  • Consider modern JavaScript features that might make some patterns unnecessary
  • Document why you chose a specific pattern for future developers
  • Test patterns thoroughly, especially their edge cases and error handling

Conclusion

Design patterns are powerful tools that help solve common programming problems elegantly. They provide a shared vocabulary for developers and proven solutions that have stood the test of time. However, remember that patterns should serve your code, not the other way around.

Tags

#Software Engineering

Share this article

Enjoying the Content?

If this article helped you, consider buying me a coffee
Your support helps me create more quality content for the community!

Buy Me a Coffee
Or simply share this article!

☕ Every coffee fuels more tutorials • 🚀 100% goes to creating better content • ❤️ Thank you for your support!

A

About Amr S.

Passionate about web development and sharing knowledge with the community. Writing about modern web technologies, best practices, and developer experiences.

TechVision