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.
Amr S.
Author & Developer

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
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
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
// 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
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
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
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
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
// 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
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
// 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
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!
☕ Every coffee fuels more tutorials • 🚀 100% goes to creating better content • ❤️ Thank you for your support!
About Amr S.
Passionate about web development and sharing knowledge with the community. Writing about modern web technologies, best practices, and developer experiences.
More from Amr S.

Mastering Object-Oriented Programming in JavaScript: Complete Fundamentals Guide
Master JavaScript OOP fundamentals with this complete guide covering classes, inheritance, encapsulation, polymorphism, and best practices with practical examples.

SOLID Principles in JavaScript: Complete Guide to Better Software Design
Learn how to apply SOLID principles in JavaScript development. complete guide with practical examples covering Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion principles.

Building Microservices with RabbitMQ: A Complete Guide
Learn how to implement message-driven microservices architecture using RabbitMQ with practical examples and best practices.