Rohit Marathe

Essential Design Patterns Every Engineer Should Know

Design patterns are battle-tested solutions to recurring problems in software design. You don't need to memorize all 23 Gang of Four patterns โ€” but knowing the essential ones will make you a significantly better engineer.

Software design patterns illustrated as interconnected architectural components

Here are the patterns I use most in production systems, with practical examples that go beyond textbook definitions.


1. Factory Pattern โ€” "Don't new Things Directly"

The Factory pattern delegates object creation to a separate method or class, so your code doesn't need to know the exact class it's creating.

Why It Matters

Imagine you're building an AI system that needs to support multiple LLM providers โ€” Claude, GPT-4, and Gemini. Without a factory, every place you create a provider is tightly coupled:

# โŒ Bad โ€” tightly coupled
if provider_name == "claude":
    client = ClaudeClient(api_key=key, model="claude-3")
elif provider_name == "gpt4":
    client = OpenAIClient(api_key=key, model="gpt-4")
elif provider_name == "gemini":
    client = GeminiClient(api_key=key, model="gemini-pro")

With a factory, creation logic lives in one place:

# โœ… Good โ€” Factory pattern
class LLMFactory:
    _providers = {
        "claude": ClaudeClient,
        "gpt4": OpenAIClient,
        "gemini": GeminiClient,
    }

    @classmethod
    def create(cls, provider: str, **kwargs) -> LLMClient:
        if provider not in cls._providers:
            raise ValueError(f"Unknown provider: {provider}")
        return cls._providers[provider](**kwargs)

# Usage โ€” clean and extensible
client = LLMFactory.create("claude", api_key=key, model="claude-3")

When to Use It


2. Observer Pattern โ€” "Notify Everyone Who Cares"

The Observer pattern lets objects subscribe to events and get notified when something changes โ€” without the publisher knowing who's listening.

Real-World Example

In a monitoring system, when a deployment completes, multiple systems need to react:

class EventBus:
    def __init__(self):
        self._subscribers: dict[str, list[Callable]] = {}

    def subscribe(self, event: str, callback: Callable):
        self._subscribers.setdefault(event, []).append(callback)

    def publish(self, event: str, data: dict):
        for callback in self._subscribers.get(event, []):
            callback(data)

# Set up observers
bus = EventBus()
bus.subscribe("deploy.complete", send_slack_notification)
bus.subscribe("deploy.complete", update_dashboard)
bus.subscribe("deploy.complete", run_smoke_tests)
bus.subscribe("deploy.failed", page_oncall_engineer)

# Publisher doesn't know or care who's listening
bus.publish("deploy.complete", {"service": "api", "version": "2.1.0"})

When to Use It


3. Strategy Pattern โ€” "Swap Algorithms at Runtime"

The Strategy pattern lets you define a family of algorithms, put each in its own class, and make them interchangeable.

Three core design patterns: Factory, Observer, and Strategy

Real-World Example

Different documents need different chunking strategies for a RAG pipeline:

from abc import ABC, abstractmethod

class ChunkingStrategy(ABC):
    @abstractmethod
    def chunk(self, text: str) -> list[str]:
        pass

class FixedSizeChunking(ChunkingStrategy):
    def __init__(self, size: int = 512, overlap: int = 50):
        self.size = size
        self.overlap = overlap

    def chunk(self, text: str) -> list[str]:
        chunks = []
        for i in range(0, len(text), self.size - self.overlap):
            chunks.append(text[i:i + self.size])
        return chunks

class SemanticChunking(ChunkingStrategy):
    def chunk(self, text: str) -> list[str]:
        # Split on paragraph boundaries for semantic coherence
        return [p.strip() for p in text.split("\n\n") if p.strip()]

class DocumentProcessor:
    def __init__(self, strategy: ChunkingStrategy):
        self.strategy = strategy

    def process(self, document: str) -> list[str]:
        return self.strategy.chunk(document)

# Swap strategies without changing the processor
processor = DocumentProcessor(SemanticChunking())
chunks = processor.process(long_document)

When to Use It


4. Singleton Pattern โ€” "One Instance to Rule Them All"

The Singleton pattern ensures a class has exactly one instance and provides a global access point to it.

When It's Actually Useful

Database connection pools, configuration managers, and logging services are legitimate Singleton use cases:

class DatabasePool:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._pool = create_connection_pool(
                host="db.example.com",
                max_connections=20
            )
        return cls._instance

    def get_connection(self):
        return self._pool.acquire()

# Both references point to the same pool
db1 = DatabasePool()
db2 = DatabasePool()
assert db1 is db2  # True โ€” same instance

โš ๏ธ The Singleton Warning

Singletons are heavily overused. Before reaching for one, ask yourself:

In modern Python, I prefer module-level instances or dependency injection over the classic Singleton pattern.


Cheat Sheet

Pattern Problem It Solves Key Signal
Factory Complex object creation if/elif chains creating objects
Observer Multiple reactions to events Several components watching for changes
Strategy Swappable algorithms if/elif chains choosing behavior
Singleton Exactly one shared instance Resource pools, config, logging

What About the Rest?

These four cover probably 70% of the patterns I use day-to-day. Other patterns worth learning next:

The best way to learn patterns isn't memorization โ€” it's recognizing the problems they solve in your own code. Next time you see a messy if/elif chain or tightly coupled components, that's your signal.


Have a favorite pattern I didn't cover? Let me know on LinkedIn.