Development

How to Use Design Patterns in Modern Web Development

Learn how to implement common design patterns in your web applications to create more maintainable, scalable, and robust code. This comprehensive guide covers the most essential patterns every developer should know.

Mistri Team
August 31, 2025
12 min read
20 views
#Design Patterns#JavaScript#TypeScript#Web Development#Architecture#Best Practices
How to Use Design Patterns in Modern Web Development

Introduction to Design Patterns

Design patterns are reusable solutions to common problems that occur in software design. They represent best practices evolved over time and provide a common vocabulary for developers to communicate effectively about software architecture.

In this comprehensive guide, we'll explore the most essential design patterns for modern web development, complete with practical examples in JavaScript and TypeScript. Whether you're building a simple website or a complex web application, understanding these patterns will help you write more maintainable and scalable code.

Why Design Patterns Matter

Design patterns offer several key benefits:

  • Code Reusability: Write once, use many times
  • Maintainability: Easier to modify and extend
  • Scalability: Handle growth and complexity
  • Team Communication: Common vocabulary
  • Best Practices: Proven solutions to common problems

Creational Patterns

Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.

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 database connections, logging, and configuration management.

class DatabaseConnection {
  private static instance: DatabaseConnection;
  private connection: any;

  private constructor() {
    // Private constructor prevents direct instantiation
    this.connection = this.initializeConnection();
  }

  public static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }

  private initializeConnection() {
    // Initialize database connection
    return { connected: true };
  }

  public query(sql: string) {
    // Execute database query
    console.log(`Executing: ${sql}`);
  }
}

// Usage
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true - same instance

2. Factory Pattern

The Factory pattern creates objects without specifying their exact class. It's useful when you need to create objects based on certain conditions or configurations.

interface PaymentProcessor {
  processPayment(amount: number): boolean;
}

class CreditCardProcessor implements PaymentProcessor {
  processPayment(amount: number): boolean {
    console.log(`Processing credit card payment: $${amount}`);
    return true;
  }
}

class PayPalProcessor implements PaymentProcessor {
  processPayment(amount: number): boolean {
    console.log(`Processing PayPal payment: $${amount}`);
    return true;
  }
}

class PaymentProcessorFactory {
  static createProcessor(type: 'creditcard' | 'paypal'): PaymentProcessor {
    switch (type) {
      case 'creditcard':
        return new CreditCardProcessor();
      case 'paypal':
        return new PayPalProcessor();
      default:
        throw new Error('Unsupported payment type');
    }
  }
}

// Usage
const processor = PaymentProcessorFactory.createProcessor('creditcard');
processor.processPayment(100);

Structural Patterns

Structural patterns deal with object composition and relationships between entities, making it easier to design systems where the relationships between objects are more flexible.

3. Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces.

// Old API
class OldAPI {
  fetchData() {
    return { data: 'old format', version: '1.0' };
  }
}

// New API interface
interface NewAPI {
  getData(): { content: string; apiVersion: string };
}

// Adapter
class APIAdapter implements NewAPI {
  private oldAPI: OldAPI;

  constructor(oldAPI: OldAPI) {
    this.oldAPI = oldAPI;
  }

  getData() {
    const oldData = this.oldAPI.fetchData();
    return {
      content: oldData.data,
      apiVersion: oldData.version
    };
  }
}

// Usage
const oldAPI = new OldAPI();
const adapter = new APIAdapter(oldAPI);
const newFormatData = adapter.getData();

Behavioral Patterns

Behavioral patterns focus on communication between objects and the flow of control through a system.

4. Observer Pattern

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

interface Observer {
  update(data: any): void;
}

class Subject {
  private observers: Observer[] = [];
  private state: any;

  addObserver(observer: Observer): void {
    this.observers.push(observer);
  }

  removeObserver(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notifyObservers(): void {
    this.observers.forEach(observer => observer.update(this.state));
  }

  setState(newState: any): void {
    this.state = newState;
    this.notifyObservers();
  }
}

class EmailNotification implements Observer {
  update(data: any): void {
    console.log(`Email: Order status changed to ${data.status}`);
  }
}

class SMSNotification implements Observer {
  update(data: any): void {
    console.log(`SMS: Order status changed to ${data.status}`);
  }
}

// Usage
const orderSubject = new Subject();
const emailNotifier = new EmailNotification();
const smsNotifier = new SMSNotification();

orderSubject.addObserver(emailNotifier);
orderSubject.addObserver(smsNotifier);

orderSubject.setState({ status: 'shipped', orderId: '12345' });

5. Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.

interface SortingStrategy {
  sort(data: number[]): number[];
}

class BubbleSort implements SortingStrategy {
  sort(data: number[]): number[] {
    console.log('Using bubble sort');
    return [...data].sort((a, b) => a - b);
  }
}

class QuickSort implements SortingStrategy {
  sort(data: number[]): number[] {
    console.log('Using quick sort');
    return [...data].sort((a, b) => a - b);
  }
}

class Sorter {
  private strategy: SortingStrategy;

  constructor(strategy: SortingStrategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy: SortingStrategy): void {
    this.strategy = strategy;
  }

  sort(data: number[]): number[] {
    return this.strategy.sort(data);
  }
}

// Usage
const data = [64, 34, 25, 12, 22, 11, 90];
const sorter = new Sorter(new BubbleSort());
console.log(sorter.sort(data));

sorter.setStrategy(new QuickSort());
console.log(sorter.sort(data));

Modern JavaScript/TypeScript Patterns

With modern JavaScript and TypeScript, we can implement design patterns more elegantly using features like modules, decorators, and advanced type system.

6. Module Pattern

The Module pattern provides a way to create private and public methods and variables, protecting parts from leaking into the global scope.

// userService.ts
export class UserService {
  private users: Map<string, any> = new Map();

  public createUser(userData: any): string {
    const id = this.generateId();
    this.users.set(id, { ...userData, id });
    return id;
  }

  public getUser(id: string): any {
    return this.users.get(id);
  }

  private generateId(): string {
    return Math.random().toString(36).substr(2, 9);
  }
}

// Export singleton instance
export const userService = new UserService();

Best Practices and Tips

  • Don't overuse patterns - use them when they solve real problems
  • Understand the trade-offs of each pattern
  • Consider your team's familiarity with patterns
  • Start simple and refactor to patterns when needed
  • Use TypeScript for better type safety with patterns

Conclusion

Design patterns are powerful tools that can significantly improve your code quality and maintainability. However, they should be used judiciously. The key is to understand when and how to apply them effectively.

Start by mastering the fundamental patterns we've covered in this guide, then gradually explore more advanced patterns as your projects grow in complexity. Remember, the goal is to write code that is not just functional, but also maintainable, scalable, and easy to understand.

Design patterns are not a silver bullet, but they are valuable tools in a developer's toolkit. Use them wisely, and they will help you build better software.
Loading...
Last updated: August 31, 2025
Continue Reading

Related Posts

Continue reading more insights from our team

Understanding the Singleton Design Pattern
8 min read
Sep 3, 2025

Understanding the Singleton Design Pattern

Learn what the Singleton Pattern is, why it's used, its pros and cons, and how to implement it in real-world applications.

Read More
How You Should Architect an Application
10 min read
Sep 2, 2025

How You Should Architect an Application

Learn the principles and best practices of software architecture. Discover how to design scalable, secure, and maintainable applications from the ground up.

Read More
How You Should Not Architect an Application
12 min read
Sep 1, 2025

How You Should Not Architect an Application

Avoid common anti-patterns and mistakes in application architecture. Learn what not to do when designing scalable, secure, and maintainable systems.

Read More