JavaScript Design Patterns Explained with Examples
October 02, 20246 min read

JavaScript Design Patterns Explained with Examples

Web developmentJavaScriptProgramming
share this article on

When we say “Design patterns are reusable solutions to commonly occurring problems in software development,” we’re talking about standardized approaches that developers have crafted over time to solve problems that come up frequently when writing software. Think of them as blueprints or templates that can be applied to different situations.

Let me give you some concrete examples:

  1. Commonly occurring problems might include:
  1. When we say these patterns lead to more maintainable code, we mean:
  1. Scalable code means:
  1. Robust code refers to:

Here’s a simple analogy: Think of design patterns like recipes in cooking. If you’re cooking a specific dish, you don’t have to figure out from scratch how to make it - you can follow a tested recipe. Similarly, when solving common programming problems, you don’t have to reinvent the wheel; you can use established design patterns.

Let’s take a quick example. Suppose you’re building an application that needs to manage user settings. You want to ensure these settings are consistent throughout the app. Instead of creating multiple instances of a settings manager, you could use the Singleton pattern:

class Settings {
    constructor() {
        if (Settings.instance) {
            return Settings.instance;
        }
        
        this.theme = 'light';
        this.fontSize = 'medium';
        Settings.instance = this;
    }
    
    changeTheme(newTheme) {
        this.theme = newTheme;
    }
}

// Usage
const settings1 = new Settings();
const settings2 = new Settings();

console.log(settings1 === settings2); // true

settings1.changeTheme('dark');
console.log(settings2.theme); // 'dark'

In this example:

Let’s explore four essential design patterns with practical, real-world examples.

1. Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is particularly useful for managing global state or resources.

Real-World Use Case: Configuration Manager

class ConfigManager {
    constructor() {
        if (ConfigManager.instance) {
            return ConfigManager.instance;
        }
        
        this.config = {
            apiUrl: 'https://api.example.com',
            timeout: 5000,
            version: '1.0.0'
        };
        
        ConfigManager.instance = this;
    }
    
    get(key) {
        return this.config[key];
    }
    
    set(key, value) {
        this.config[key] = value;
    }
}

// Usage
const configManager1 = new ConfigManager();
const configManager2 = new ConfigManager();

console.log(configManager1 === configManager2); // true

configManager1.set('apiUrl', 'https://api.newexample.com');
console.log(configManager2.get('apiUrl')); // https://api.newexample.com

In this example, the ConfigManager ensures that application configuration is consistent across different parts of your application.

2. Factory Pattern

The Factory pattern provides an interface for creating objects but allows subclasses to decide which class to instantiate. It’s useful when you need to create objects without exposing the creation logic.

Real-World Use Case: Payment Method Factory

class PaymentMethod {
    processPayment(amount) {
        throw new Error('processPayment method must be implemented');
    }
}

class CreditCardPayment extends PaymentMethod {
    processPayment(amount) {
        console.log(`Processing credit card payment of $${amount}`);
    }
}

class PayPalPayment extends PaymentMethod {
    processPayment(amount) {
        console.log(`Processing PayPal payment of $${amount}`);
    }
}

class PaymentMethodFactory {
    createPaymentMethod(type) {
        switch (type) {
            case 'credit-card':
                return new CreditCardPayment();
            case 'paypal':
                return new PayPalPayment();
            default:
                throw new Error('Invalid payment method');
        }
    }
}

// Usage
const factory = new PaymentMethodFactory();
const creditCardPayment = factory.createPaymentMethod('credit-card');
const paypalPayment = factory.createPaymentMethod('paypal');

creditCardPayment.processPayment(100); // Processing credit card payment of $100
paypalPayment.processPayment(50); // Processing PayPal payment of $50

This pattern is excellent for payment processing systems where different payment methods need to be handled uniformly.

3. Observer Pattern

The Observer pattern defines a one-to-many dependency between objects. When one object changes state, all its dependents are notified and updated automatically.

Real-World Use Case: News Feed

class NewsFeed {
    constructor() {
        this.subscribers = [];
    }
    
    subscribe(subscriber) {
        this.subscribers.push(subscriber);
    }
    
    unsubscribe(subscriber) {
        this.subscribers = this.subscribers.filter(sub => sub !== subscriber);
    }
    
    notify(news) {
        this.subscribers.forEach(subscriber => subscriber.update(news));
    }
}

class NewsSubscriber {
    constructor(name) {
        this.name = name;
    }
    
    update(news) {
        console.log(`${this.name} received news: ${news}`);
    }
}

// Usage
const newsFeed = new NewsFeed();
const subscriber1 = new NewsSubscriber('John');
const subscriber2 = new NewsSubscriber('Jane');

newsFeed.subscribe(subscriber1);
newsFeed.subscribe(subscriber2);

newsFeed.notify('Breaking: JavaScript is awesome!');
// John received news: Breaking: JavaScript is awesome!
// Jane received news: Breaking: JavaScript is awesome!

newsFeed.unsubscribe(subscriber1);
newsFeed.notify('Another news update');
// Jane received news: Another news update

This pattern is perfect for implementing features like notifications, event handling, or any scenario where you need to maintain a list of dependents to notify.

4. Module Pattern

The Module pattern encapsulates ‘privacy’, state and organization using closures. It provides a way to wrap public and private methods and variables in a single object.

Real-World Use Case: User Authentication Module

const UserAuth = (function() {
    // Private variables and methods
    let currentUser = null;
    
    function validateUsername(username) {
        return username.length >= 3;
    }
    
    function validatePassword(password) {
        return password.length >= 8;
    }
    
    // Public API
    return {
        login(username, password) {
            if (validateUsername(username) && validatePassword(password)) {
                currentUser = username;
                return `${username} successfully logged in`;
            }
            return 'Invalid username or password';
        },
        
        logout() {
            const username = currentUser;
            currentUser = null;
            return `${username} logged out`;
        },
        
        getCurrentUser() {
            return currentUser;
        }
    };
})();

// Usage
console.log(UserAuth.login('john', 'password123')); // john successfully logged in
console.log(UserAuth.getCurrentUser()); // john
console.log(UserAuth.logout()); // john logged out

The Module pattern is excellent for creating a public API while keeping certain variables and methods private and inaccessible from the outside.

Conclusion

Design patterns are powerful tools in a developer’s arsenal. By understanding and correctly implementing these patterns, you can write more maintainable and scalable code. Remember, patterns should be used judiciously - not every problem requires a design pattern solution. The key is to understand when and where to apply them effectively.

As you continue your JavaScript journey, you’ll encounter situations where these patterns can significantly improve your code structure. Practice implementing them in your projects, and you’ll become more proficient at recognizing scenarios where each pattern can be beneficial.

Happy coding!

Back to Articles