LogicLoop Logo
LogicLoop
LogicLoop / clean-code-principles / 7 Essential Design Patterns Every Developer Should Master in 2023
clean-code-principles April 12, 2025 11 min read

7 Essential Software Design Patterns Every Developer Should Master for Better Code

Eleanor Park

Eleanor Park

Developer Advocate

7 Essential Design Patterns Every Developer Should Master in 2023

Software design patterns are proven solutions to recurring programming problems. Whether you realize it or not, you're likely already using many of them in your daily coding. These patterns are language-agnostic blueprints that help you write cleaner, more maintainable code regardless of your tech stack or platform.

In this comprehensive guide, we'll explore seven essential design patterns that every developer should know, categorized into three main types: creational, structural, and behavioral patterns. Understanding these patterns will significantly improve your code quality and help you communicate more effectively with other developers.

Understanding Design Pattern Categories

Before diving into specific patterns, it's important to understand the three main categories they fall into:

  • Creational Patterns: Focus on object creation mechanisms, providing flexibility in how objects are instantiated
  • Structural Patterns: Deal with how objects relate to each other, helping build larger structures from individual components
  • Behavioral Patterns: Handle communication between objects, defining how they interact and distribute responsibilities

These categories were formalized in the seminal 1994 book "Design Patterns: Elements of Reusable Object-Oriented Software" by the "Gang of Four" (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides), which documented 23 design patterns that remain highly relevant today.

Creational Pattern #1: Singleton

The Singleton pattern ensures a class has only one instance while providing global access to it. Despite being controversial in some circles, it's invaluable in specific scenarios.

Singleton pattern implementation showing centralized logging across application components
Singleton pattern implementation showing centralized logging across application components

When to Use Singleton

Use the Singleton pattern when you need exactly one instance of a class that must be accessible globally, such as:

  • Logging systems (to ensure consistent formatting and file access)
  • Database connection pools (to manage resources efficiently)
  • Configuration managers (to maintain consistent settings)

Singleton Implementation Example

TYPESCRIPT
class Logger {
  private static instance: Logger;
  private logFile: string;

  private constructor() {
    this.logFile = 'app.log';
    // Initialize logging system
  }

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

  public log(message: string): void {
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] ${message}`);
    // Write to log file
  }
}

// Usage
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();

// Both variables reference the same instance
console.log(logger1 === logger2); // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

Singleton Trade-offs

  • Advantages: Guaranteed single instance and global access
  • Disadvantages: Difficult to test (mocking challenges), potential threading issues in multi-threaded environments, and essentially functions as a glorified global variable

Remember that Singletons should be used sparingly, only when a single instance guarantee is genuinely required—not just for convenient global state access.

Creational Pattern #2: Builder

The Builder pattern separates the construction of complex objects from their representation, allowing the same construction process to create different representations.

When to Use Builder

Use the Builder pattern when dealing with objects that have numerous parameters (especially optional ones) or when object construction involves multiple steps.

Builder Implementation Example

TYPESCRIPT
class RequestBuilder {
  private url: string = '';
  private method: string = 'GET';
  private headers: Record<string, string> = {};
  private body: any = null;
  private timeout: number = 30000;
  private retries: number = 0;

  public withUrl(url: string): RequestBuilder {
    this.url = url;
    return this;
  }

  public withMethod(method: string): RequestBuilder {
    this.method = method;
    return this;
  }

  public withHeader(key: string, value: string): RequestBuilder {
    this.headers[key] = value;
    return this;
  }

  public withBody(body: any): RequestBuilder {
    this.body = body;
    return this;
  }

  public withTimeout(timeout: number): RequestBuilder {
    this.timeout = timeout;
    return this;
  }

  public withRetries(retries: number): RequestBuilder {
    this.retries = retries;
    return this;
  }

  public build(): HttpRequest {
    return new HttpRequest(
      this.url,
      this.method,
      this.headers,
      this.body,
      this.timeout,
      this.retries
    );
  }
}

// Usage
const request = new RequestBuilder()
  .withUrl('https://api.example.com/users')
  .withMethod('POST')
  .withHeader('Content-Type', 'application/json')
  .withHeader('Authorization', 'Bearer token123')
  .withBody({ name: 'John', email: '[email protected]' })
  .withTimeout(5000)
  .build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

The Builder pattern makes complex object creation more readable and maintainable. Instead of a constructor with many parameters, you can chain methods in any order, skipping optional ones, and the code reads almost like natural language.

Builder Pattern Benefits

  • Improves readability with method chaining that reads like natural language
  • Makes complex object creation more maintainable
  • Allows adding new options without breaking existing code
  • Enables step-by-step construction (like following a recipe)

Creational Pattern #3: Factory

The Factory pattern delegates object creation to a separate class, hiding the instantiation logic from the client code.

Factory Pattern implementation showing creation of different user types with centralized instantiation logic
Factory Pattern implementation showing creation of different user types with centralized instantiation logic

When to Use Factory

Use the Factory pattern when you need to create different types of related objects without exposing the creation logic to the client code.

Factory Implementation Example

TYPESCRIPT
// User types
class AdminUser {
  constructor(public id: number, public name: string) {}
  // Admin-specific methods
}

class ModeratorUser {
  constructor(public id: number, public name: string) {}
  // Moderator-specific methods
}

class RegularUser {
  constructor(public id: number, public name: string) {}
  // Regular user methods
}

// User factory
class UserFactory {
  public static createUser(type: string, id: number, name: string) {
    switch (type.toLowerCase()) {
      case 'admin':
        return new AdminUser(id, name);
      case 'moderator':
        return new ModeratorUser(id, name);
      case 'regular':
        return new RegularUser(id, name);
      default:
        throw new Error(`User type ${type} not supported`);
    }
  }
}

// Usage
const admin = UserFactory.createUser('admin', 1, 'John');
const moderator = UserFactory.createUser('moderator', 2, 'Sarah');
const user = UserFactory.createUser('regular', 3, 'Mike');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

Factory Pattern Benefits

  • Centralizes object creation logic in one place
  • Makes code more maintainable (changes to creation logic only happen in one place)
  • Simplifies client code by abstracting away complex instantiation
  • Enables adding logging, caching, or object pooling without changing client code

Structural Pattern #1: Facade

The Facade pattern provides a simplified interface to a complex subsystem of classes, making it easier to use.

When to Use Facade

Use the Facade pattern when you need to provide a simple interface to a complex system, or when you want to decouple your code from a third-party library.

Facade Implementation Example

TYPESCRIPT
// Complex subsystem classes
class AudioPlayer {
  public turnOn(): void { /* complex implementation */ }
  public loadAudio(file: string): void { /* complex implementation */ }
  public play(): void { /* complex implementation */ }
}

class Display {
  public turnOn(): void { /* complex implementation */ }
  public setResolution(width: number, height: number): void { /* complex implementation */ }
  public showVisualizer(): void { /* complex implementation */ }
}

class Amplifier {
  public turnOn(): void { /* complex implementation */ }
  public setVolume(level: number): void { /* complex implementation */ }
  public setEqualizer(bass: number, mid: number, treble: number): void { /* complex implementation */ }
}

// Facade
class MusicPlayerFacade {
  private audioPlayer: AudioPlayer;
  private display: Display;
  private amplifier: Amplifier;

  constructor() {
    this.audioPlayer = new AudioPlayer();
    this.display = new Display();
    this.amplifier = new Amplifier();
  }

  public playMusic(file: string): void {
    // Simple interface hiding complex operations
    this.audioPlayer.turnOn();
    this.display.turnOn();
    this.amplifier.turnOn();
    
    this.display.setResolution(1920, 1080);
    this.display.showVisualizer();
    
    this.amplifier.setVolume(70);
    this.amplifier.setEqualizer(80, 50, 60);
    
    this.audioPlayer.loadAudio(file);
    this.audioPlayer.play();
  }
}

// Usage
const musicPlayer = new MusicPlayerFacade();
musicPlayer.playMusic('song.mp3'); // Simple interface to complex operations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

The Facade pattern simplifies interaction with complex systems by providing a unified, higher-level interface. It's like using a remote control instead of manually adjusting each component of your entertainment system.

Structural Pattern #2: Adapter

The Adapter pattern allows incompatible interfaces to work together by wrapping an instance of one class with a new interface.

When to Use Adapter

Use the Adapter pattern when you need to integrate with existing systems or third-party libraries that have incompatible interfaces with your code.

Adapter Implementation Example

TYPESCRIPT
// Target interface our code expects to work with
interface ModernPaymentProcessor {
  processPayment(amount: number, cardDetails: { number: string, cvv: string, expiry: string }): boolean;
}

// Existing legacy system with incompatible interface
class LegacyPaymentSystem {
  public authenticatePayment(cardNo: string, securityCode: string, expiryDate: string): boolean {
    // Legacy authentication logic
    console.log('Authenticating payment with legacy system...');
    return true;
  }
  
  public transferAmount(amount: number): string {
    // Legacy transfer logic
    console.log(`Transferring $${amount} with legacy system...`);
    return 'LEGACY_TXN_123';
  }
}

// Adapter that makes legacy system work with our modern interface
class PaymentSystemAdapter implements ModernPaymentProcessor {
  private legacySystem: LegacyPaymentSystem;
  
  constructor() {
    this.legacySystem = new LegacyPaymentSystem();
  }
  
  public processPayment(amount: number, cardDetails: { number: string, cvv: string, expiry: string }): boolean {
    // Adapt the modern interface to work with legacy system
    const isAuthenticated = this.legacySystem.authenticatePayment(
      cardDetails.number, 
      cardDetails.cvv, 
      cardDetails.expiry
    );
    
    if (isAuthenticated) {
      const transactionId = this.legacySystem.transferAmount(amount);
      console.log(`Payment successful with transaction ID: ${transactionId}`);
      return true;
    }
    
    return false;
  }
}

// Usage
const paymentProcessor: ModernPaymentProcessor = new PaymentSystemAdapter();

paymentProcessor.processPayment(99.99, {
  number: '4111111111111111',
  cvv: '123',
  expiry: '12/25'
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

Adapter Pattern Benefits

  • Enables integration with incompatible systems without changing their code
  • Promotes reusability of existing code
  • Simplifies system migration by creating compatibility layers
  • Provides flexibility when working with third-party libraries

Behavioral Pattern #1: Observer

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.

When to Use Observer

Use the Observer pattern when changes to one object require changing others, and you don't know how many objects need to change or when their set varies dynamically.

Observer Implementation Example

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

// Subject (observable)
class NewsPublisher {
  private observers: Observer[] = [];
  private latestNews: string = '';
  
  public subscribe(observer: Observer): void {
    const isExist = this.observers.includes(observer);
    if (!isExist) {
      this.observers.push(observer);
    }
  }
  
  public unsubscribe(observer: Observer): void {
    const observerIndex = this.observers.indexOf(observer);
    if (observerIndex !== -1) {
      this.observers.splice(observerIndex, 1);
    }
  }
  
  public notify(): void {
    for (const observer of this.observers) {
      observer.update(this.latestNews);
    }
  }
  
  public publishNews(news: string): void {
    this.latestNews = news;
    this.notify();
  }
}

// Concrete observers
class EmailSubscriber implements Observer {
  private email: string;
  
  constructor(email: string) {
    this.email = email;
  }
  
  public update(news: string): void {
    console.log(`Sending email to ${this.email} with news: ${news}`);
  }
}

class MobileAppSubscriber implements Observer {
  private userId: string;
  
  constructor(userId: string) {
    this.userId = userId;
  }
  
  public update(news: string): void {
    console.log(`Sending push notification to user ${this.userId}: ${news}`);
  }
}

// Usage
const publisher = new NewsPublisher();

const emailSub1 = new EmailSubscriber('[email protected]');
const emailSub2 = new EmailSubscriber('[email protected]');
const mobileSub = new MobileAppSubscriber('user_42');

publisher.subscribe(emailSub1);
publisher.subscribe(emailSub2);
publisher.subscribe(mobileSub);

publisher.publishNews('Breaking: Design Patterns Simplify Development!');
// All subscribers will be notified

publisher.unsubscribe(emailSub1);
publisher.publishNews('Update: Strategy Pattern Voted Most Useful!');
// Only emailSub2 and mobileSub will be notified
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

The Observer pattern is widely used in event-driven programming and is the foundation of many MVC frameworks. It's also the core pattern behind reactive programming paradigms.

Behavioral Pattern #2: Strategy

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

Strategy Pattern implementation showing dynamic selection of transport methods with error handling
Strategy Pattern implementation showing dynamic selection of transport methods with error handling

When to Use Strategy

Use the Strategy pattern when you have multiple algorithms for a specific task and you want to switch between them dynamically at runtime based on the situation.

Strategy Implementation Example

TYPESCRIPT
// Strategy interface
interface PaymentStrategy {
  pay(amount: number): boolean;
}

// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
  private cardNumber: string;
  private cvv: string;
  private expiryDate: string;
  
  constructor(cardNumber: string, cvv: string, expiryDate: string) {
    this.cardNumber = cardNumber;
    this.cvv = cvv;
    this.expiryDate = expiryDate;
  }
  
  public pay(amount: number): boolean {
    console.log(`Paying $${amount} using Credit Card: ${this.cardNumber}`);
    // Processing logic
    return true;
  }
}

class PayPalPayment implements PaymentStrategy {
  private email: string;
  private password: string;
  
  constructor(email: string, password: string) {
    this.email = email;
    this.password = password;
  }
  
  public pay(amount: number): boolean {
    console.log(`Paying $${amount} using PayPal account: ${this.email}`);
    // Processing logic
    return true;
  }
}

class CryptoPayment implements PaymentStrategy {
  private walletAddress: string;
  
  constructor(walletAddress: string) {
    this.walletAddress = walletAddress;
  }
  
  public pay(amount: number): boolean {
    console.log(`Paying $${amount} equivalent in crypto to wallet: ${this.walletAddress}`);
    // Processing logic
    return true;
  }
}

// Context
class ShoppingCart {
  private items: { name: string, price: number }[] = [];
  private paymentStrategy?: PaymentStrategy;
  
  public addItem(item: { name: string, price: number }): void {
    this.items.push(item);
  }
  
  public setPaymentStrategy(strategy: PaymentStrategy): void {
    this.paymentStrategy = strategy;
  }
  
  public checkout(): boolean {
    if (!this.paymentStrategy) {
      throw new Error('Payment strategy not set');
    }
    
    const total = this.items.reduce((sum, item) => sum + item.price, 0);
    return this.paymentStrategy.pay(total);
  }
}

// Usage
const cart = new ShoppingCart();
cart.addItem({ name: 'Design Patterns Book', price: 49.99 });
cart.addItem({ name: 'Mechanical Keyboard', price: 159.99 });

// User chooses payment method
const creditCardStrategy = new CreditCardPayment('4111111111111111', '123', '12/25');
cart.setPaymentStrategy(creditCardStrategy);

// Process payment
cart.checkout();

// Later, user decides to use a different payment method
const paypalStrategy = new PayPalPayment('[email protected]', 'password123');
cart.setPaymentStrategy(paypalStrategy);
cart.checkout();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

The Strategy pattern is one of the most versatile and useful patterns. It enables selecting an algorithm's implementation at runtime, promoting flexibility and clean code by eliminating complex conditional logic.

Conclusion: Applying Design Patterns Effectively

Design patterns are powerful tools that can significantly improve your code quality, but they should be applied judiciously. Here are some guidelines for using them effectively:

  • Don't force patterns where they don't fit—use them to solve actual problems
  • Understand the trade-offs of each pattern before implementing it
  • Start with the simplest solution and refactor to patterns when complexity increases
  • Use patterns to communicate intent with other developers
  • Combine patterns when appropriate to solve complex problems

By mastering these seven fundamental design patterns, you'll develop a stronger intuition for writing maintainable, flexible code that can adapt to changing requirements. Remember that design patterns aren't about showing off your knowledge—they're practical tools to solve real-world programming problems more effectively.

As you gain experience, you'll start recognizing patterns in existing codebases and naturally incorporating them into your own work, leading to more robust and professional software solutions.

Let's Watch!

7 Essential Design Patterns Every Developer Should Master in 2023

Ready to enhance your neural network?

Access our quantum knowledge cores and upgrade your programming abilities.

Initialize Training Sequence
L
LogicLoop

High-quality programming content and resources for developers of all skill levels. Our platform offers comprehensive tutorials, practical code examples, and interactive learning paths designed to help you master modern development concepts.

© 2025 LogicLoop. All rights reserved.