Design Patterns That Actually Matter: 7 Patterns Every Developer Uses Daily

Forget the Gang of Four book with 23 patterns you will never use. These 7 design patterns appear in every codebase, every framework, and every system design interview. Real Python and TypeScript examples, zero academic fluff.

Design Patterns That Actually Matter: 7 Patterns Every Developer Uses Daily illustration
On this page9 sections

Design patterns have a reputation problem. The Gang of Four book describes 23 patterns, most developers memorize a few for interviews, and then never consciously use them. But the truth is you use design patterns every day - you just do not call them by name.

This guide covers the 7 patterns that genuinely appear in production code, with practical examples in Python and TypeScript. Use the SOLID principles refactoring examples to judge class boundaries, and the separation-of-concerns architecture guide to place each pattern at the right layer.

1. Strategy Pattern: Swappable Algorithms

Replace conditional logic with interchangeable objects that encapsulate different behaviors. You have already used this if you have ever passed a function as an argument.

# Without Strategy: growing if/else chain
def calculate_shipping(order, method):
    if method == "standard":
        return order.weight * 0.5
    elif method == "express":
        return order.weight * 1.5 + 10
    elif method == "overnight":
        return order.weight * 3.0 + 25
    elif method == "drone":       # New method = modify this function
        return 50.0

# With Strategy: each algorithm is a separate callable
from typing import Protocol

class ShippingStrategy(Protocol):
    def calculate(self, order) -> float: ...

class StandardShipping:
    def calculate(self, order) -> float:
        return order.weight * 0.5

class ExpressShipping:
    def calculate(self, order) -> float:
        return order.weight * 1.5 + 10

class OvernightShipping:
    def calculate(self, order) -> float:
        return order.weight * 3.0 + 25

# Usage: swap strategy without changing caller
def checkout(order, shipping: ShippingStrategy):
    cost = shipping.calculate(order)
    return cost

checkout(order, ExpressShipping())  # Easy to add new strategies

Where you see it: Sorting algorithms (key functions), authentication strategies (Passport.js), payment processors, serialization formats.

2. Observer Pattern: Event-Driven Communication

When one object changes, notify all interested objects automatically. This is the foundation of event-driven programming.

# Python implementation
class EventEmitter:
    def __init__(self):
        self._listeners: dict[str, list] = {}

    def on(self, event: str, callback):
        self._listeners.setdefault(event, []).append(callback)

    def emit(self, event: str, data=None):
        for callback in self._listeners.get(event, []):
            callback(data)

# Usage
events = EventEmitter()

# Register observers
events.on("order:created", lambda order: send_email(order))
events.on("order:created", lambda order: update_inventory(order))
events.on("order:created", lambda order: notify_warehouse(order))

# Trigger event - all observers run
events.emit("order:created", new_order)
// TypeScript: Angular uses this everywhere (RxJS Subjects)
import { Subject } from 'rxjs';

class OrderService {
  private orderCreated = new Subject<Order>();
  orderCreated$ = this.orderCreated.asObservable();

  createOrder(data: OrderData): Order {
    const order = this.save(data);
    this.orderCreated.next(order);  // Notify all subscribers
    return order;
  }
}

// Subscribers
orderService.orderCreated$.subscribe(order => sendEmail(order));
orderService.orderCreated$.subscribe(order => updateAnalytics(order));

Where you see it: DOM events (addEventListener), RxJS, Node.js EventEmitter, React state management, message queues.

3. Factory Pattern: Object Creation Logic

Encapsulate object creation when the exact type depends on runtime conditions. Instead of the caller knowing every possible class, the factory decides.

# Without Factory: caller must know every type
def process_payment(method, amount):
    if method == "credit_card":
        processor = CreditCardProcessor()
    elif method == "paypal":
        processor = PayPalProcessor()
    elif method == "crypto":
        processor = CryptoProcessor()
    processor.charge(amount)

# With Factory: creation logic in one place
class PaymentFactory:
    _processors = {
        "credit_card": CreditCardProcessor,
        "paypal": PayPalProcessor,
        "crypto": CryptoProcessor,
    }

    @classmethod
    def create(cls, method: str) -> PaymentProcessor:
        processor_cls = cls._processors.get(method)
        if not processor_cls:
            raise ValueError(f"Unknown payment method: {method}")
        return processor_cls()

# Usage: clean and extensible
processor = PaymentFactory.create("paypal")
processor.charge(99.99)

# Adding a new method: just register it
PaymentFactory._processors["apple_pay"] = ApplePayProcessor

Where you see it: Django’s ORM (model instances from query results), React.createElement, database connection factories, logger factories.

4. Decorator Pattern: Adding Behavior Without Modification

Wrap an object to add functionality without changing its interface. Python decorators are the most famous implementation, but the pattern is broader than the syntax.

# Python decorators ARE the decorator pattern
import functools
import time
import logging

def retry(max_attempts=3, delay=1):
    """Decorator that retries a function on failure."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    time.sleep(delay * (2 ** attempt))
            return None
        return wrapper
    return decorator

def log_calls(func):
    """Decorator that logs function calls."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        logging.info(f"{func.__name__} returned {result}")
        return result
    return wrapper

# Stack decorators: each wraps the previous
@retry(max_attempts=3)
@log_calls
def fetch_user_data(user_id):
    return api.get(f"/users/{user_id}")
// TypeScript: class-based decorator pattern
interface DataSource {
  read(): string[];
}

class FileDataSource implements DataSource {
  read(): string[] { return readFile('data.csv'); }
}

// Decorator: adds caching without modifying FileDataSource
class CachedDataSource implements DataSource {
  private cache: string[] | null = null;
  constructor(private wrapped: DataSource) {}

  read(): string[] {
    if (!this.cache) {
      this.cache = this.wrapped.read();
    }
    return this.cache;
  }
}

// Decorator: adds logging
class LoggedDataSource implements DataSource {
  constructor(private wrapped: DataSource) {}

  read(): string[] {
    console.log('Reading data...');
    const result = this.wrapped.read();
    console.log(`Read ${result.length} records`);
    return result;
  }
}

// Compose: logged + cached + file
const source = new LoggedDataSource(
  new CachedDataSource(
    new FileDataSource()
  )
);

Where you see it: Python decorators (@app.route, @login_required), Express middleware, Java annotations, TypeScript decorators.

5. Singleton Pattern: One Instance, Global Access

Ensure a class has exactly one instance. Controversial but practical for shared resources like database connections, loggers, and configuration.

# Python: module-level is already a singleton
# config.py
class Config:
    def __init__(self):
        self.db_url = os.environ["DATABASE_URL"]
        self.redis_url = os.environ["REDIS_URL"]
        self.debug = os.environ.get("DEBUG", "false") == "true"

config = Config()  # Created once when module is imported

# All other files:
from config import config  # Same instance everywhere
// TypeScript: Angular services are singletons by default
@Injectable({ providedIn: 'root' })  // One instance for the entire app
export class AuthService {
  private currentUser = signal<User | null>(null);

  // Every component that injects AuthService gets the SAME instance
  login(credentials: Credentials) { ... }
  logout() { ... }
}

Where you see it: Angular services (providedIn: root), Python modules, database connection pools, logging.getLogger().

6. Adapter Pattern: Making Incompatible Interfaces Work Together

Wrap an existing class so it matches the interface your code expects. Essential when integrating third-party libraries or legacy systems.

# Your code expects this interface:
class PaymentGateway(Protocol):
    def charge(self, amount: float, currency: str) -> dict: ...

# But the Stripe SDK has a different interface:
# stripe.PaymentIntent.create(amount=1000, currency="usd")  # amount in cents!

# Adapter: wraps Stripe to match your interface
class StripeAdapter:
    def charge(self, amount: float, currency: str) -> dict:
        # Convert dollars to cents (Stripe's format)
        intent = stripe.PaymentIntent.create(
            amount=int(amount * 100),
            currency=currency.lower(),
        )
        return {
            "id": intent.id,
            "status": "success" if intent.status == "succeeded" else "pending",
            "amount": amount,
        }

# Another adapter for PayPal (different API entirely)
class PayPalAdapter:
    def charge(self, amount: float, currency: str) -> dict:
        order = paypal.Order.create(
            purchase_units=[{"amount": {"value": str(amount), "currency_code": currency}}]
        )
        return {
            "id": order.id,
            "status": "success" if order.status == "COMPLETED" else "pending",
            "amount": amount,
        }

# Your code works with any adapter:
def process_payment(gateway: PaymentGateway, amount: float):
    result = gateway.charge(amount, "USD")
    return result

Where you see it: ORM adapters (SQLAlchemy supports PostgreSQL, MySQL, SQLite through adapters), logging handlers, API client wrappers.

7. Builder Pattern: Complex Object Construction

Construct complex objects step by step instead of through a constructor with 15 parameters.

# Without Builder: constructor with too many parameters
query = SQLQuery(
    table="users",
    columns=["name", "email"],
    where="age > 18",
    order_by="name",
    order_dir="ASC",
    limit=100,
    offset=0,
    join_table="orders",
    join_condition="users.id = orders.user_id",
    group_by="name",
    having="COUNT(*) > 5",
)

# With Builder: readable, chainable construction
class QueryBuilder:
    def __init__(self, table: str):
        self._table = table
        self._columns = ["*"]
        self._conditions = []
        self._order = None
        self._limit = None

    def select(self, *columns):
        self._columns = list(columns)
        return self  # Return self for chaining

    def where(self, condition: str):
        self._conditions.append(condition)
        return self

    def order_by(self, column: str, direction: str = "ASC"):
        self._order = f"{column} {direction}"
        return self

    def limit(self, n: int):
        self._limit = n
        return self

    def build(self) -> str:
        query = f"SELECT {', '.join(self._columns)} FROM {self._table}"
        if self._conditions:
            query += " WHERE " + " AND ".join(self._conditions)
        if self._order:
            query += f" ORDER BY {self._order}"
        if self._limit:
            query += f" LIMIT {self._limit}"
        return query

# Usage: reads like English
query = (QueryBuilder("users")
    .select("name", "email")
    .where("age > 18")
    .where("status = 'active'")
    .order_by("name")
    .limit(100)
    .build()
)

Where you see it: ORM query builders (Django QuerySet, SQLAlchemy), HTTP request builders, test data builders, UI component builders.

Pattern Selection Guide

Problem Pattern
Multiple algorithms for the same task Strategy
Notify multiple objects of state changes Observer
Object creation depends on runtime data Factory
Add behavior without modifying existing code Decorator
Exactly one shared instance needed Singleton
Integrate incompatible third-party interfaces Adapter
Complex object with many configuration options Builder

Key Takeaways

  • Patterns are tools, not goals - do not force a pattern where a simple function would do
  • Strategy eliminates if/else chains - use it when you have multiple algorithms for the same task
  • Observer decouples producers from consumers - the foundation of event-driven architecture
  • Factory centralizes creation logic - add new types without modifying calling code
  • Decorator adds behavior without inheritance - compose small, focused wrappers
  • Singleton is just a module-level instance in Python and providedIn: root in Angular
  • Adapter wraps third-party code to match your interfaces - essential for swappable integrations
  • Builder replaces constructors with 10+ parameters - readable, chainable, self-documenting

The best code uses patterns without naming them. If you write a function that takes a callback, you are using Strategy. If you emit events, you are using Observer. If you wrap a class to add logging, you are using Decorator. The patterns are already in your code - knowing their names helps you communicate about them and apply them intentionally.

Share this article

Stuck on implementation?

Get private, 1-on-1 help with system design, performance, scaling, or any technical challenge.

Book a Session

Related Production Resources

Course

Free learning tracks

Turn this guide into a structured production engineering path.

Lab

Interactive engineering labs

Practice the same ideas through scenario-based simulators.

Reference

Production cheatsheets

Keep the operational commands and checks nearby.

Glossary

Key terms

Review the vocabulary behind the architecture.

Discussion

Questions, corrections, or production notes? Add them here so other learners can benefit.

Comments load on demand

To keep this article fast and private by default, the GitHub-powered discussion loads only when you reach this section.

Prefer GitHub? Open the project discussions directly .

Continue Reading

Related practical guides from the same production engineering path.

Tutorials 11 min read

Regex Demystified: From Fear to Fluency in 20 Minutes

Regular expressions look like line noise until you learn to read them left to right. This guide breaks down regex visually, covers the 20 patterns that solve 95% of real-world tasks, and includes a copy-paste cheat sheet.

Regex Python
Tutorials 17 min read

Git Internals: How Git Actually Works Under the Hood

Git is not magic - it is a content-addressable object database with refs, an index, reflogs, and a graph of snapshots. Explore blobs, trees, commits, tags, branches, rebases, packfiles, and recovery.

Git Version Control
Tutorials 18 min read

SOLID Principles in Practice: Write Code That Doesn't Rot

Learn the 5 SOLID principles through real-world Python and TypeScript examples. See bad code, understand why it breaks, then refactor it into clean, maintainable architecture.

SOLID Design Patterns
Backend 16 min read

Separation of Concerns: The Architecture Principle Behind Every Clean Codebase

Understand why mixing business logic with database queries and HTTP handling creates unmaintainable code. Learn SoC through layered architecture, MVC, microservices, and frontend patterns - with real refactoring examples.

Architecture Clean Code