Blog/architecture

The 7 Design Patterns Every Developer Needs to Know (And How to Actually Understand Them)

S
Schoolab Team
12 min

The 7 Design Patterns Every Developer Needs to Know (And How to Actually Understand Them)

If you have ever stared at your own code and thought, "There has to be a cleaner way to do this," you are probably right.

In software engineering, we don't always need to reinvent the wheel. Since the 1990s (thanks to the famous "Gang of Four" book), developers have used Design Patterns—repeatable solutions to common coding problems. Think of them not as strict rules, but as blueprints you can customize to fix specific architectural issues.

Here are the 7 most essential design patterns that will make your code cleaner, scalable, and easier to debug.

Part 1: Creational Patterns

These patterns are all about how you create objects (the "things" in your code). They help you create objects in a way that suits the situation.

1. The Singleton Pattern

The Concept: Imagine a country having two presidents at the same time. Chaos, right? The Singleton pattern ensures that a class has only one single instance and provides a global point of access to it. No matter how many times you try to create a new one, you get the exact same existing one.

Real-World Analogy: Your computer's Clipboard. You can copy text from Chrome and paste it into Word. There is only one clipboard shared across the whole system. If there were two, you wouldn't know which one you were pasting from.

When to Use It:

  • Logging: You want one file where all error logs go.
  • Database Connections: You don't want to open a new connection for every single query; you want to reuse one efficient connection pool.

Example Implementation:

javascript
class DatabaseConnection {
  private static instance: DatabaseConnection | null = null;

  private constructor() {
    // Private constructor prevents direct instantiation
  }

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

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

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

2. The Builder Pattern

The Concept: Some objects are complex. If you have a class that requires 10 different parameters to start (size, color, weight, speed, etc.), your code looks messy. The Builder pattern lets you construct complex objects step-by-step.

Real-World Analogy: Ordering at Subway. You don't walk in and grab a pre-made sandwich. You say, "Start with Italian bread. Add turkey. Add cheese. Toast it. Add mayo." You build the final product one step at a time.

When to Use It:

  • Complex Configurations: When creating things like HTTP requests where you might need headers, a body, cookies, and timeouts, but not always all at once.

Example Implementation:

javascript
class HttpRequest {
  constructor(
    public url: string,
    public method: string,
    public headers?: Record<string, string>,
    public body?: string,
    public timeout?: number
  ) {}
}

class HttpRequestBuilder {
  private url: string = '';
  private method: string = 'GET';
  private headers: Record<string, string> = {};
  private body?: string;
  private timeout?: number;

  setUrl(url: string) {
    this.url = url;
    return this;
  }

  setMethod(method: string) {
    this.method = method;
    return this;
  }

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

  setBody(body: string) {
    this.body = body;
    return this;
  }

  setTimeout(timeout: number) {
    this.timeout = timeout;
    return this;
  }

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

// Usage - much cleaner!
const request = new HttpRequestBuilder()
  .setUrl('https://api.example.com/users')
  .setMethod('POST')
  .addHeader('Content-Type', 'application/json')
  .setBody('{"name": "John"}')
  .setTimeout(5000)
  .build();

3. The Factory Pattern

The Concept: This is one of the most popular patterns. Instead of creating objects directly (using the new keyword), you ask a "Factory" to create them for you. You tell the factory what you want, and it handles the logic of how to create it.

Real-World Analogy: A Hiring Agency. If you need a graphic designer, you don't go out and interview everyone yourself. You call the agency (The Factory) and say, "I need a Designer." They handle the vetting and send you the professional. You don't care how they found them; you just get the result.

When to Use It:

  • Cross-Platform Apps: When your code runs on both iPhone and Android. You ask for a "Button," and the Factory decides whether to give you an iOS-style button or an Android-style button.

Example Implementation:

javascript
interface Button {
  render(): void;
  onClick(): void;
}

class IOSButton implements Button {
  render() {
    console.log('Rendering iOS button');
  }
  onClick() {
    console.log('iOS button clicked');
  }
}

class AndroidButton implements Button {
  render() {
    console.log('Rendering Android button');
  }
  onClick() {
    console.log('Android button clicked');
  }
}

class ButtonFactory {
  static createButton(platform: 'ios' | 'android'): Button {
    if (platform === 'ios') {
      return new IOSButton();
    } else {
      return new AndroidButton();
    }
  }
}

// Usage
const button = ButtonFactory.createButton('ios');
button.render(); // "Rendering iOS button"

Part 2: Structural Patterns

These patterns are about how you assemble objects and classes into larger structures, like using Lego bricks to build a castle.

4. The Facade Pattern

The Concept: "Facade" means the face of a building. In code, this pattern provides a simplified interface to a complex set of classes or libraries. It hides the messy code behind a clean "mask."

Real-World Analogy: Ordering from Amazon. When you click "Buy Now," a million things happen: inventory is checked, your credit card is charged, a shipping label is printed, and a warehouse robot is notified. You don't see any of that. You just see one simple button. That button is the Facade.

When to Use It:

  • Wrappers: When you are using a very complex third-party library, and you want to wrap it in a simple class so your team doesn't have to learn the complicated library.

Example Implementation:

javascript
// Complex subsystems
class InventoryService {
  checkAvailability(productId: string): boolean {
    console.log(`Checking inventory for ${productId}`);
    return true;
  }
}

class PaymentService {
  processPayment(amount: number): boolean {
    console.log(`Processing payment of $${amount}`);
    return true;
  }
}

class ShippingService {
  createLabel(address: string): string {
    console.log(`Creating shipping label for ${address}`);
    return 'SHIP-12345';
  }
}

// Facade - simple interface
class OrderFacade {
  private inventory: InventoryService;
  private payment: PaymentService;
  private shipping: ShippingService;

  constructor() {
    this.inventory = new InventoryService();
    this.payment = new PaymentService();
    this.shipping = new ShippingService();
  }

  placeOrder(productId: string, amount: number, address: string): boolean {
    if (!this.inventory.checkAvailability(productId)) {
      return false;
    }
    if (!this.payment.processPayment(amount)) {
      return false;
    }
    const label = this.shipping.createLabel(address);
    console.log(`Order placed! Shipping label: ${label}`);
    return true;
  }
}

// Usage - simple!
const order = new OrderFacade();
order.placeOrder('PROD-123', 99.99, '123 Main St');

5. The Adapter Pattern

The Concept: This allows incompatible interfaces to work together. It acts as a bridge between two things that wouldn't normally connect.

Real-World Analogy: A Travel Power Adapter. If you take your American laptop to Europe, the plug won't fit the wall. You need an adapter that translates the shape of your plug into the shape of the wall socket.

When to Use It:

  • Legacy Code: You have an old system that outputs data in XML, but your new fancy dashboard only reads JSON. You write an Adapter to translate XML to JSON on the fly.

Example Implementation:

javascript
// Old system - returns XML
class LegacyDataService {
  getData(): string {
    return '<user><name>John</name><age>30</age></user>';
  }
}

// New system - expects JSON
interface ModernDataService {
  getData(): { name: string; age: number };
}

// Adapter - converts XML to JSON
class XMLToJSONAdapter implements ModernDataService {
  private legacyService: LegacyDataService;

  constructor(legacyService: LegacyDataService) {
    this.legacyService = legacyService;
  }

  getData(): { name: string; age: number } {
    const xml = this.legacyService.getData();
    // Simple XML parsing (in real world, use a proper parser)
    const nameMatch = xml.match(/<name>(.*?)</name>/);
    const ageMatch = xml.match(/<age>(.*?)</age>/);
    
    return {
      name: nameMatch ? nameMatch[1] : '',
      age: ageMatch ? parseInt(ageMatch[1]) : 0
    };
  }
}

// Usage
const legacy = new LegacyDataService();
const adapter = new XMLToJSONAdapter(legacy);
const data = adapter.getData(); // { name: 'John', age: 30 }

Part 3: Behavioral Patterns

These patterns handle communication between objects. They figure out "who is responsible for what."

6. The Strategy Pattern

The Concept: This allows you to define a family of algorithms (ways of doing things) and make them interchangeable. You can switch the "strategy" at runtime without breaking the code.

Real-World Analogy: Google Maps. You want to go to the airport. The app offers you different strategies: "Fastest Route," "Avoid Tolls," or "Walking." The destination (Goal) is the same, but the method (Strategy) changes based on your preference.

When to Use It:

  • Payment Processing: Your website accepts PayPal, Credit Card, and Bitcoin. These are three different strategies for the same action: "Pay." You can swap them easily depending on what the user clicks.

Example Implementation:

javascript
// Strategy interface
interface PaymentStrategy {
  pay(amount: number): void;
}

// Concrete strategies
class CreditCardStrategy implements PaymentStrategy {
  constructor(private cardNumber: string) {}

  pay(amount: number) {
    console.log(`Paid $${amount} using credit card ending in ${this.cardNumber.slice(-4)}`);
  }
}

class PayPalStrategy implements PaymentStrategy {
  constructor(private email: string) {}

  pay(amount: number) {
    console.log(`Paid $${amount} using PayPal account ${this.email}`);
  }
}

class BitcoinStrategy implements PaymentStrategy {
  constructor(private walletAddress: string) {}

  pay(amount: number) {
    console.log(`Paid $${amount} using Bitcoin wallet ${this.walletAddress}`);
  }
}

// Context
class PaymentProcessor {
  private strategy: PaymentStrategy;

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

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

  processPayment(amount: number) {
    this.strategy.pay(amount);
  }
}

// Usage
const processor = new PaymentProcessor(new CreditCardStrategy('1234567890123456'));
processor.processPayment(100);

processor.setStrategy(new PayPalStrategy('[email protected]'));
processor.processPayment(50);

7. The Observer Pattern

The Concept: This defines a subscription mechanism. When one object (the Subject) changes, it automatically notifies all other objects (Observers) that are "watching" it.

Real-World Analogy: YouTube Subscription. You don't refresh a YouTuber's page every 5 minutes to see if they posted. You subscribe. When they upload (The Event), YouTube notifies you (The Observer).

When to Use It:

  • Chat Apps: When a new message arrives, the app needs to notify the notification bar, update the unread count badge, and play a sound. The "New Message" event triggers all these observers.

Example Implementation:

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

// Subject
class MessageService {
  private observers: Observer[] = [];
  private messages: string[] = [];

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

  unsubscribe(observer: Observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data: any) {
    this.observers.forEach(observer => observer.update(data));
  }

  addMessage(message: string) {
    this.messages.push(message);
    this.notify({ type: 'NEW_MESSAGE', message });
  }
}

// Concrete observers
class NotificationBar implements Observer {
  update(data: any) {
    if (data.type === 'NEW_MESSAGE') {
      console.log(`🔔 New message: ${data.message}`);
    }
  }
}

class UnreadBadge implements Observer {
  private count: number = 0;

  update(data: any) {
    if (data.type === 'NEW_MESSAGE') {
      this.count++;
      console.log(`📬 Unread messages: ${this.count}`);
    }
  }
}

class SoundPlayer implements Observer {
  update(data: any) {
    if (data.type === 'NEW_MESSAGE') {
      console.log('🔊 Playing notification sound');
    }
  }
}

// Usage
const messageService = new MessageService();
const notificationBar = new NotificationBar();
const unreadBadge = new UnreadBadge();
const soundPlayer = new SoundPlayer();

messageService.subscribe(notificationBar);
messageService.subscribe(unreadBadge);
messageService.subscribe(soundPlayer);

messageService.addMessage('Hello, world!');
// All observers are notified automatically!

Conclusion: Which One Should You Learn First?

You don't need to memorize all 23 original patterns immediately. Start with Singleton, Factory, and Observer. These three appear in almost every modern application, from simple mobile apps to massive enterprise systems.

Mastering these patterns moves you from being a "coder" who types lines to a "software engineer" who designs solutions.

Quick Reference Guide

  • Singleton: One instance, global access (logging, database connections)
  • Builder: Step-by-step object construction (complex configurations)
  • Factory: Delegate object creation (cross-platform apps)
  • Facade: Simplify complex interfaces (wrapper classes)
  • Adapter: Bridge incompatible interfaces (legacy code integration)
  • Strategy: Interchangeable algorithms (payment methods, routing)
  • Observer: Subscribe and notify (event systems, UI updates)

Remember: Design patterns are tools, not rules. Use them when they solve a real problem, not just because they exist. Happy coding!