Design Patterns

Part of Software Engineering

Design Patterns

Design Patterns are reusable solutions to common software design problems. They represent best practices evolved over time by experienced software developers.

Why Design Patterns Matter

Think of design patterns as proven architectural blueprints for software. Just as architects use patterns for buildings (like "open plan" or "split level"), developers use patterns for software structure. They provide shared vocabulary, prevent reinventing the wheel, and create maintainable, scalable code.

Pattern Categories

Creational

Deal with object creation mechanisms

Common Patterns

SingletonFactory MethodAbstract FactoryBuilderPrototype

Primary Purpose

Control object creation, provide flexibility in what gets created

When to Use

When object creation is complex or needs to be controlled

Structural

Deal with object composition and relationships

Common Patterns

AdapterDecoratorFacadeProxyCompositeBridgeFlyweight

Primary Purpose

Form larger structures from individual objects

When to Use

When you need to compose objects or manage relationships

Behavioral

Deal with object interaction and responsibility distribution

Common Patterns

ObserverStrategyCommandStateTemplate MethodIteratorMediator

Primary Purpose

Define communication between objects and assign responsibilities

When to Use

When objects need to communicate or algorithms need to vary

Creational Patterns

Control object creation mechanisms, trying to create objects in a manner suitable to the situation.

Singleton

Ensures a class has only one instance and provides global access

Intent

Control object creation, limiting to single instance

Structure

Private constructor, static instance, static access method

Common Use Cases

Database connections
Logging
Configuration
Cache

Example Implementation

class Database {
  private static instance: Database;
  private connection: Connection;
  
  private constructor() {
    this.connection = new Connection();
  }
  
  public static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new Database();
    }
    return Database.instance;
  }
  
  public query(sql: string): Result {
    return this.connection.execute(sql);
  }
}

Pros

  • Controlled access
  • Reduced memory usage
  • Global state management

Cons

  • Difficult to test
  • Hidden dependencies
  • Violates Single Responsibility

Structural Patterns

Deal with object composition and relationships, forming larger structures from individual objects.

Adapter

Allows incompatible interfaces to work together

Intent

Convert interface of class into another interface clients expect

Structure

Target interface, Adaptee, Adapter

Common Use Cases

Legacy integration
Third-party libraries
API wrappers

Example Implementation

// Old system
class OldPrinter {
  printText(text: string) {
    console.log(`Old printer: ${text}`);
  }
}

// New interface
interface Printer {
  print(content: string): void;
}

// Adapter
class PrinterAdapter implements Printer {
  private oldPrinter: OldPrinter;
  
  constructor(oldPrinter: OldPrinter) {
    this.oldPrinter = oldPrinter;
  }
  
  print(content: string): void {
    this.oldPrinter.printText(content);
  }
}

Pros

  • Reuse legacy code
  • Single Responsibility
  • Flexibility

Cons

  • Additional complexity
  • Performance overhead
  • Overuse warning

Behavioral Patterns

Deal with object interaction and responsibility distribution among objects.

Observer

Defines one-to-many dependency between objects

Intent

Define one-to-many dependency between objects

Structure

Subject, Observer interface, Concrete observers

Common Use Cases

Event handling
Pub/Sub systems
MVC architecture

Example Implementation

interface Observer {
  update(temperature: number, humidity: number): void;
}

interface Subject {
  registerObserver(o: Observer): void;
  removeObserver(o: Observer): void;
  notifyObservers(): void;
}

class WeatherStation implements Subject {
  private observers: Observer[] = [];
  private temperature: number = 0;
  private humidity: number = 0;
  
  registerObserver(o: Observer) {
    this.observers.push(o);
  }
  
  removeObserver(o: Observer) {
    const index = this.observers.indexOf(o);
    if (index > -1) this.observers.splice(index, 1);
  }
  
  notifyObservers() {
    this.observers.forEach(observer => 
      observer.update(this.temperature, this.humidity)
    );
  }
  
  setMeasurements(temp: number, humidity: number) {
    this.temperature = temp;
    this.humidity = humidity;
    this.notifyObservers();
  }
}

Pros

  • Loose coupling
  • Broadcast communication
  • Dynamic relationships

Cons

  • Memory leaks
  • Unexpected updates
  • Performance

Real-world Applications

Singleton

Database Connection Pool

Manages limited database connections across application

Key Benefits
Resource optimizationThread safetyCentralized management
class ConnectionPool {
  private static instance: ConnectionPool;
  private connections: Connection[] = [];
  private MAX_CONNECTIONS = 10;
  
  private constructor() {
    this.initializePool();
  }
  
  public static getInstance(): ConnectionPool {
    if (!ConnectionPool.instance) {
      ConnectionPool.instance = new ConnectionPool();
    }
    return ConnectionPool.instance;
  }
  
  public getConnection(): Connection {
    return this.connections.pop() || new Connection();
  }
}
Observer

Stock Market Notifications

Notifies multiple investors when stock prices change

Key Benefits
Real-time updatesDecoupled componentsScalable
class StockMarket {
  private investors: Investor[] = [];
  private stockPrice: number = 100;
  
  subscribe(investor: Investor) {
    this.investors.push(investor);
  }
  
  setPrice(price: number) {
    this.stockPrice = price;
    this.investors.forEach(investor => 
      investor.update(this.stockPrice)
    );
  }
}
Strategy

E-commerce Shipping

Different shipping strategies (Standard, Express, Overnight)

Key Benefits
Flexible pricingEasy to add new methodsClean code
interface ShippingStrategy {
  calculate(weight: number): number;
}

class StandardShipping implements ShippingStrategy {
  calculate(weight: number): number {
    return weight * 1.5;
  }
}

class Order {
  private shipping: ShippingStrategy;
  
  setShipping(strategy: ShippingStrategy) {
    this.shipping = strategy;
  }
  
  calculateShipping(weight: number): number {
    return this.shipping.calculate(weight);
  }
}
Decorator

Web Request Middleware

Adds functionality to HTTP requests (logging, auth, compression)

Key Benefits
ModularChainableRuntime composition
interface Handler {
  handle(request: Request): Response;
}

class LoggingDecorator implements Handler {
  constructor(private next: Handler) {}
  
  handle(request: Request): Response {
    console.log(`Request: ${request.url}`);
    return this.next.handle(request);
  }
}

Common Anti-patterns

These are common bad practices that design patterns help you avoid.

God Object

One class does too many things, knows too much

Symptoms
3000+ lines of codeMany dependenciesFrequent changes
Fix

Apply Single Responsibility Principle, split into smaller classes

Example
// BAD: God object
class Application {
  // Handles everything: UI, business logic, database, logging, etc.
}

// GOOD: Separate responsibilities
class UserService { }
class DatabaseService { }
class Logger { }

Spaghetti Code

Unstructured, tangled code with no clear separation

Symptoms
Goto statementsDeep nestingGlobal variables
Fix

Apply design patterns, refactor with clear structure

Example
// BAD: Spaghetti code
if (condition1) {
  if (condition2) {
    // ... 10 more nested levels
  }
}

// GOOD: Clean structure
function processCondition1() {
  if (!condition1) return;
  processCondition2();
}

Golden Hammer

Using same solution (pattern) for every problem

Symptoms
Singleton everywhereFactory for simple objectsOver-engineered
Fix

Choose patterns based on problem, not habit

Example
// BAD: Using Singleton unnecessarily
class Logger {
  private static instance: Logger;
  // ... but we only need one logger instance?
}

// GOOD: Simple static class might suffice
class Logger {
  static log(message: string) { console.log(message); }
}

Circular Dependency

Two or more classes depend on each other

Symptoms
Import cyclesCompilation errorsTight coupling
Fix

Use Dependency Injection, introduce interfaces, refactor

Example
// BAD: Circular dependency
class A { constructor(private b: B) {} }
class B { constructor(private a: A) {} }

// GOOD: Break the cycle
interface IA { /* methods */ }
class A implements IA { constructor(private b: B) {} }
class B { constructor(private a: IA) {} }

Pattern Selection Guide

ProblemConsider These PatternsQuick Decision
Need to ensure only one instanceSingletonUse for: DB connections, config, logging
Creating complex objectsBuilder, Factory Method, Abstract FactoryBuilder for step-by-step, Factory for families
Adding behavior dynamicallyDecoratorBetter than inheritance for runtime changes
Simplifying complex systemFacadeWhen clients need simple interface to complex code
Algorithm selection at runtimeStrategyReplace conditional logic with objects
One-to-many notificationsObserverFor event-driven, pub/sub systems
Need undo/redo functionalityCommandEncapsulate requests as objects
Object behavior depends on stateStateReplace state conditionals with objects

Golden Rule

Don't force patterns. Use them when they solve actual problems in your code. Patterns should emerge from refactoring, not be applied prematurely.

Best Practices

Start Simple

Begin with simple code. Apply patterns during refactoring when you see duplication, complexity, or inflexibility. Don't over-engineer from the start.

Know SOLID Principles

Patterns work best with SOLID principles. Understand Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.

Choose Wisely

Not every problem needs a pattern. Sometimes a simple function or class is enough. Use patterns to solve specific problems, not as a goal in themselves.

Refactor Gradually

Apply patterns incrementally. Refactor small parts at a time, ensuring tests pass. This reduces risk and makes changes manageable.

Test Your Pattern Knowledge

Design Patterns Quiz

Question 1 of 6

Which pattern ensures a class has only one instance?

Further Resources

Essential Books

  • Design Patterns: Elements of Reusable OO SoftwareGang of Four
  • Head First Design PatternsBeginner Friendly
  • Patterns of Enterprise Application ArchitectureMartin Fowler
  • Clean ArchitectureRobert C. Martin

Online Resources

  • Refactoring.GuruFree
  • SourceMaking PatternsFree
  • Dofactory .NET PatternsFree
  • Pattern Language (C2 Wiki)Community

Practice Problems

Beginner
  • • Logger with Singleton
  • • Payment system with Strategy
  • • File system with Composite
  • • Coffee shop with Decorator
Intermediate
  • • E-commerce with Factory
  • • Text editor with Command
  • • Weather station with Observer
  • • Game character with State
Advanced
  • • ORM with Proxy/Decorator
  • • Workflow engine with Template
  • • Microservices with Facade
  • • Plugin system with Strategy