Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Design Patterns Map - All 23 Gang of Four Patterns
Time to Master: 2-3 hours | Prerequisites: OOP basics | Interview Frequency: Very High
Pattern Selection Tip: Don’t force patterns! If simple code solves the problem, use simple code. Patterns add complexity that must be justified with real benefits.

📊 Pattern Overview

Design patterns are proven solutions to common software design problems. They are not inventions — they are discoveries. The Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) catalogued these patterns in 1994 by studying what experienced developers did repeatedly. Think of patterns as a shared vocabulary: when you say “let’s use a Strategy here,” every engineer on the team immediately understands the structure, the trade-offs, and the intent. There are 23 classic patterns divided into 3 categories:

Creational (5)

How objects are createdSingleton, Factory, Abstract Factory, Builder, Prototype

Structural (7)

How objects are composedAdapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

Behavioral (11)

How objects communicateStrategy, Observer, Command, State, Template, Iterator, Mediator, Chain of Responsibility, Visitor, Memento, Interpreter
Interview Focus: You don’t need to memorize all 23! Focus on: Singleton, Factory, Strategy, Observer, State, Decorator, Command - these cover 90% of LLD interviews.

🎯 Pattern Decision Tree

Use this to quickly identify which pattern fits your problem:
Need to create objects?
├── Only ONE instance needed? ───────────────────► SINGLETON
├── Don't know exact class at compile time? ────► FACTORY
├── Object has many optional parameters? ───────► BUILDER
└── Need to copy existing objects? ─────────────► PROTOTYPE

Need to compose/structure objects?
├── Incompatible interfaces? ───────────────────► ADAPTER
├── Add behavior without subclassing? ──────────► DECORATOR
├── Simplify complex subsystem? ────────────────► FACADE
├── Tree-like hierarchies? ─────────────────────► COMPOSITE
└── Control access to object? ──────────────────► PROXY

Need to manage object behavior/communication?
├── Swap algorithms at runtime? ────────────────► STRATEGY
├── Notify multiple objects of changes? ────────► OBSERVER
├── Object behavior changes with state? ────────► STATE
├── Queue and undo operations? ─────────────────► COMMAND
├── Traverse collection without exposing internals? ► ITERATOR
└── Reduce coupling between objects? ───────────► MEDIATOR

🔵 Creational Patterns

How objects are created

Singleton

One instance only

Factory

Create without specifying class

Builder

Step-by-step construction

1. Singleton Pattern

When to Use: Database connections, Configuration, Logging, Thread pools, Caches - anything where only ONE instance should exist.
Ensures a class has only one instance with global access. This is the simplest pattern to understand but the easiest to misuse. Use it when having multiple instances would cause real problems — such as multiple database connection pools competing for resources, or multiple configuration objects with conflicting settings. The key insight is that Singleton controls identity (there is exactly one), not just access (it is globally reachable). If you only need global access, a module-level variable or dependency injection is often a better choice.
import threading

class DatabaseConnection:
    _instance = None
    _lock = threading.Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:  # Double-checked locking
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialize()
        return cls._instance
    
    def _initialize(self):
        self.connection = self._create_connection()
    
    def _create_connection(self):
        return "Database Connection"

# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
assert db1 is db2  # Same instance
Use When: Database connections, Configuration, Logging, Thread pools, Caches
Singleton Anti-patterns:
  • Makes unit testing harder (global state)
  • Can hide dependencies
  • Consider dependency injection instead for better testability

2. Factory Pattern

Creates objects without specifying exact classes. Factory encapsulates the “which class to instantiate” decision, so the calling code depends only on the interface, not the concrete type. This directly supports the Open/Closed Principle: when you add a new notification channel, you update the factory’s registry — the rest of your codebase never changes.
from abc import ABC, abstractmethod

# Product interface -- all notifications share this contract (Abstraction)
class Notification(ABC):
    @abstractmethod
    def send(self, message: str):
        pass

# Concrete products
class EmailNotification(Notification):
    def send(self, message: str):
        print(f"Sending email: {message}")

class SMSNotification(Notification):
    def send(self, message: str):
        print(f"Sending SMS: {message}")

class PushNotification(Notification):
    def send(self, message: str):
        print(f"Sending push: {message}")

# Factory
class NotificationFactory:
    @staticmethod
    def create(channel: str) -> Notification:
        factories = {
            "email": EmailNotification,
            "sms": SMSNotification,
            "push": PushNotification
        }
        if channel not in factories:
            raise ValueError(f"Unknown channel: {channel}")
        return factories[channel]()

# Usage
notification = NotificationFactory.create("email")
notification.send("Hello!")
Use When: Object creation logic is complex, Multiple types share interface

3. Builder Pattern

Constructs complex objects step by step. Builder shines when an object has many optional parameters and you want a readable, fluent API instead of a constructor with 15 arguments. The key insight is separating construction from representation — the same building process can produce different results. In real-world codebases, you will see Builder used for query builders (SQLAlchemy), HTTP request builders, and configuration objects.
class Pizza:
    def __init__(self):
        self.size = None
        self.cheese = False
        self.pepperoni = False
        self.mushrooms = False
    
    def __str__(self):
        toppings = []
        if self.cheese: toppings.append("cheese")
        if self.pepperoni: toppings.append("pepperoni")
        if self.mushrooms: toppings.append("mushrooms")
        return f"{self.size} pizza with {', '.join(toppings)}"

class PizzaBuilder:
    def __init__(self):
        self.pizza = Pizza()
    
    def set_size(self, size: str):
        self.pizza.size = size
        return self
    
    def add_cheese(self):
        self.pizza.cheese = True
        return self
    
    def add_pepperoni(self):
        self.pizza.pepperoni = True
        return self
    
    def add_mushrooms(self):
        self.pizza.mushrooms = True
        return self
    
    def build(self) -> Pizza:
        return self.pizza

# Usage - fluent interface
pizza = (PizzaBuilder()
         .set_size("large")
         .add_cheese()
         .add_pepperoni()
         .build())
print(pizza)  # large pizza with cheese, pepperoni
Use When: Object has many optional parameters, Step-by-step construction

Structural Patterns

How objects are composed

4. Adapter Pattern

Makes incompatible interfaces work together.
# Existing interface (what client expects)
class ModernPayment(ABC):
    @abstractmethod
    def pay(self, amount: float) -> bool:
        pass

# Legacy system (what we have)
class LegacyPaymentGateway:
    def make_payment(self, dollars: int, cents: int) -> str:
        return f"Paid ${dollars}.{cents:02d}"

# Adapter
class PaymentAdapter(ModernPayment):
    def __init__(self, legacy: LegacyPaymentGateway):
        self.legacy = legacy
    
    def pay(self, amount: float) -> bool:
        dollars = int(amount)
        cents = int((amount - dollars) * 100)
        result = self.legacy.make_payment(dollars, cents)
        return "Paid" in result

# Usage
legacy_gateway = LegacyPaymentGateway()
payment = PaymentAdapter(legacy_gateway)
payment.pay(29.99)
Use When: Integrating legacy code, Third-party library integration

5. Decorator Pattern

Adds behavior to objects dynamically.
class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float:
        pass
    
    @abstractmethod
    def description(self) -> str:
        pass

class SimpleCoffee(Coffee):
    def cost(self) -> float:
        return 2.0
    
    def description(self) -> str:
        return "Coffee"

class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee

class MilkDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.5
    
    def description(self) -> str:
        return self._coffee.description() + ", Milk"

class SugarDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.2
    
    def description(self) -> str:
        return self._coffee.description() + ", Sugar"

# Usage - stack decorators
coffee = SimpleCoffee()
coffee = MilkDecorator(coffee)
coffee = SugarDecorator(coffee)
print(f"{coffee.description()}: ${coffee.cost()}")
# Coffee, Milk, Sugar: $2.7
Use When: Add features without subclassing, Compose behaviors

6. Facade Pattern

Provides simple interface to complex subsystem.
# Complex subsystems
class VideoFile:
    def __init__(self, filename):
        self.filename = filename

class CodecFactory:
    def extract(self, file):
        return "codec"

class AudioMixer:
    def fix(self, audio):
        return "fixed_audio"

class VideoConverter:
    def convert(self, video, codec):
        return "converted_video"

# Facade - simple interface
class VideoConversionFacade:
    def convert(self, filename: str, format: str) -> str:
        file = VideoFile(filename)
        codec = CodecFactory().extract(file)
        audio = AudioMixer().fix(file)
        result = VideoConverter().convert(file, codec)
        return f"Converted {filename} to {format}"

# Usage - client doesn't know about subsystems
facade = VideoConversionFacade()
result = facade.convert("video.mp4", "avi")
Use When: Simplify complex systems, Provide unified API

Behavioral Patterns

How objects communicate

7. Strategy Pattern

Defines a family of interchangeable algorithms. Strategy is arguably the most important pattern for LLD interviews because it directly implements the Open/Closed Principle. The core idea: extract varying behavior into its own class hierarchy, then inject the specific variant at runtime. Whenever you see a decision that might change (pricing rules, sorting algorithms, payment processing), Strategy is your go-to pattern. It turns compile-time decisions into runtime decisions, giving you flexibility without modifying existing code.
class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount: float) -> str:
        pass

class CreditCardPayment(PaymentStrategy):
    def __init__(self, card_number: str):
        self.card_number = card_number
    
    def pay(self, amount: float) -> str:
        return f"Paid ${amount} with card ending {self.card_number[-4:]}"

class PayPalPayment(PaymentStrategy):
    def __init__(self, email: str):
        self.email = email
    
    def pay(self, amount: float) -> str:
        return f"Paid ${amount} via PayPal ({self.email})"

class CryptoPayment(PaymentStrategy):
    def __init__(self, wallet: str):
        self.wallet = wallet
    
    def pay(self, amount: float) -> str:
        return f"Paid ${amount} in crypto to {self.wallet[:8]}..."

# Context
class ShoppingCart:
    def __init__(self):
        self.items = []
        self.payment_strategy = None
    
    def set_payment_strategy(self, strategy: PaymentStrategy):
        self.payment_strategy = strategy
    
    def checkout(self) -> str:
        total = sum(item.price for item in self.items)
        return self.payment_strategy.pay(total)

# Usage - swap strategies at runtime
cart = ShoppingCart()
cart.set_payment_strategy(CreditCardPayment("1234567890123456"))
cart.checkout()
Use When: Multiple algorithms for same task, Switch behavior at runtime

8. Observer Pattern

Notifies multiple objects about state changes. Observer implements a one-to-many dependency so that when one object changes state, all dependents are notified automatically. This is the foundation of event-driven architectures, UI frameworks (React’s state management), and pub/sub systems. The critical design insight is decoupling: the subject (publisher) does not know or care about the specific observers (subscribers). This means you can add new notification channels, analytics hooks, or audit logging without touching the core business logic.
class Subject(ABC):
    @abstractmethod
    def attach(self, observer):
        pass
    
    @abstractmethod
    def detach(self, observer):
        pass
    
    @abstractmethod
    def notify(self):
        pass

class Observer(ABC):
    @abstractmethod
    def update(self, subject):
        pass

class Stock(Subject):
    def __init__(self, symbol: str, price: float):
        self.symbol = symbol
        self._price = price
        self._observers = []
    
    @property
    def price(self):
        return self._price
    
    @price.setter
    def price(self, value):
        self._price = value
        self.notify()
    
    def attach(self, observer):
        self._observers.append(observer)
    
    def detach(self, observer):
        self._observers.remove(observer)
    
    def notify(self):
        for observer in self._observers:
            observer.update(self)

class StockAlert(Observer):
    def __init__(self, name: str):
        self.name = name
    
    def update(self, subject):
        print(f"{self.name}: {subject.symbol} is now ${subject.price}")

# Usage
apple = Stock("AAPL", 150.0)
investor1 = StockAlert("Investor 1")
investor2 = StockAlert("Investor 2")

apple.attach(investor1)
apple.attach(investor2)

apple.price = 155.0  # Both investors notified
Use When: One-to-many dependencies, Event systems

9. State Pattern

Object behavior changes based on internal state. State pattern eliminates sprawling if/elif chains that check the current state before deciding what to do. Instead, each state becomes its own class with its own behavior. The context object delegates to the current state, and state transitions happen by swapping the state object. This is the go-to pattern for modeling lifecycles: order processing (Pending, Confirmed, Shipped, Delivered), elevator systems (Moving, Stopped, Maintenance), and vending machines (Idle, HasMoney, Dispensing). When interviewers hear you say “I’ll model this as a state machine,” they know you are thinking like a senior engineer.
class OrderState(ABC):
    @abstractmethod
    def next(self, order):
        pass
    
    @abstractmethod
    def prev(self, order):
        pass
    
    @abstractmethod
    def status(self) -> str:
        pass

class PendingState(OrderState):
    def next(self, order):
        order.state = ProcessingState()
    
    def prev(self, order):
        print("Already at initial state")
    
    def status(self) -> str:
        return "Pending"

class ProcessingState(OrderState):
    def next(self, order):
        order.state = ShippedState()
    
    def prev(self, order):
        order.state = PendingState()
    
    def status(self) -> str:
        return "Processing"

class ShippedState(OrderState):
    def next(self, order):
        order.state = DeliveredState()
    
    def prev(self, order):
        order.state = ProcessingState()
    
    def status(self) -> str:
        return "Shipped"

class DeliveredState(OrderState):
    def next(self, order):
        print("Already delivered")
    
    def prev(self, order):
        print("Cannot go back from delivered")
    
    def status(self) -> str:
        return "Delivered"

class Order:
    def __init__(self):
        self.state = PendingState()
    
    def next_state(self):
        self.state.next(self)
    
    def prev_state(self):
        self.state.prev(self)
    
    def status(self):
        return self.state.status()

# Usage
order = Order()
print(order.status())  # Pending
order.next_state()
print(order.status())  # Processing
order.next_state()
print(order.status())  # Shipped
Use When: Object behavior depends on state, State-specific logic is complex

Pattern Selection Guide

ProblemPattern
Need single instanceSingleton
Create objects without knowing classFactory
Complex object constructionBuilder
Integrate incompatible interfacesAdapter
Add features dynamicallyDecorator
Simplify complex subsystemFacade
Interchangeable algorithmsStrategy
Notify of state changesObserver
Behavior changes with stateState

🔥 Top Patterns for Interviews

These patterns appear most frequently in LLD interviews:

Must-Know (80% of interviews)

PatternCommon Use Cases
FactoryVehicle types, Payment methods, Notification channels
StrategyPayment processing, Shipping calculation, Pricing rules
ObserverStock price alerts, Order status, Pub/Sub systems
StateOrder lifecycle, Elevator, Vending machine, ATM
SingletonDatabase, Configuration, Logger

Good to Know (20% of interviews)

PatternCommon Use Cases
BuilderComplex configurations, Query builders
DecoratorMiddleware, Permission layers
AdapterLegacy integration, Third-party APIs
CommandUndo/Redo, Transaction logs
FacadeComplex subsystem APIs

📚 Pattern Diagrams

SINGLETON                    FACTORY                      STRATEGY
┌─────────────┐             ┌─────────────┐             ┌─────────────┐
│  Singleton  │             │   Creator   │             │   Context   │
├─────────────┤             ├─────────────┤             ├─────────────┤
│ -instance   │             │+createProd()│──creates──▷│ -strategy   │
├─────────────┤             └─────────────┘             ├─────────────┤
│+getInstance │                   │                    │+setStrategy │
└─────────────┘                   ▽                    │+execute()   │
                           ┌─────────────┐             └──────┬──────┘
                           │   Product   │                    │
                           └─────────────┘                    ▽
                                 △                   ┌────────────────┐
                          ┌──────┴──────┐            │ <<interface>>  │
                     ┌────┴────┐   ┌────┴────┐       │   Strategy     │
                     │ProductA │   │ProductB │       ├────────────────┤
                     └─────────┘   └─────────┘       │+execute()      │
                                                     └────────────────┘

                                                     ┌──────┴──────┐
                                                 ┌───┴───┐   ┌───┴───┐
                                                 │StratA │   │StratB │
                                                 └───────┘   └───────┘

OBSERVER                     STATE                        DECORATOR
┌─────────────┐             ┌─────────────┐             ┌─────────────┐
│   Subject   │             │   Context   │             │  Component  │
├─────────────┤             ├─────────────┤             ├─────────────┤
│ -observers  │             │ -state      │             │+operation() │
├─────────────┤             ├─────────────┤             └──────┬──────┘
│+attach()    │             │+setState()  │                    △
│+notify()    │             │+request()   │             ┌──────┴──────┐
└──────┬──────┘             └──────┬──────┘        ┌────┴────┐   ┌────┴────┐
       │                           │               │Concrete │   │Decorator│
       ▽                           ▽               │Component│   ├─────────┤
┌────────────────┐         ┌───────────────┐       └─────────┘   │-wrapped │
│ <<interface>>  │         │ <<interface>> │                     │+op()    │
│   Observer     │         │    State      │                     └────┬────┘
├────────────────┤         ├───────────────┤                          △
│+update()       │         │+handle()      │                   ┌──────┴──────┐
└────────────────┘         └───────────────┘               ┌───┴───┐   ┌───┴───┐
       △                          △                        │DecorA │   │DecorB │
   ┌───┴───┐               ┌──────┴──────┐                 └───────┘   └───────┘
   │ConcObs│           ┌───┴───┐   ┌───┴───┐
   └───────┘           │StateA │   │StateB │
                       └───────┘   └───────┘

📝 Pattern Comparison Table

PatternIntentUse WhenAvoid When
SingletonOne instance globallyDB connection, ConfigNeed testability, multiple instances
FactoryCreate objects without specifying classMultiple product typesOnly one concrete class
BuilderComplex object constructionMany optional parametersSimple objects
StrategySwap algorithms at runtimeMultiple ways to do somethingOnly one algorithm
ObserverNotify subscribers of changesDecoupled event handlingFew, known subscribers
StateObject behavior changes with stateFinite state machineSimple conditionals suffice
DecoratorAdd behavior dynamicallyLayered functionalityInheritance works fine
FacadeSimplify complex subsystemsComplex library integrationSimple interfaces already
CommandEncapsulate requests as objectsUndo/redo, queuingSimple direct calls

💡 Interview Tips

  • Do: “I’ll use Factory here because we need to create vehicles without knowing the exact type at compile time”
  • Don’t: “Let me apply all 23 GoF patterns to show I know them”
Pro tip: Wait for the right moment. If you’re designing payment methods, naturally mention Strategy pattern.
Every pattern has costs - always mention them:
PatternBenefitCost
SingletonGlobal access, one instanceHard to test, hidden dependencies
FactoryDecoupled creationMore abstraction layers
ObserverLoose couplingMemory leaks if not detached
StrategySwappable algorithmsMore classes to manage
DecoratorFlexible compositionComplex debugging
Always explain why the benefit outweighs the cost for YOUR use case.
Patterns often work together:
  • Factory + Strategy: Create strategies via factory
  • Observer + Singleton: Global event bus
  • State + Factory: Create states via factory
  • Decorator + Factory: Create decorators dynamically
  • Command + Memento: Undo with snapshots
  • “Why did you use X pattern here?”
  • “What’s the difference between Strategy and State?”
  • “How would you add a new payment method?” (Strategy)
  • “How would you implement undo/redo?” (Command + Memento)
  • “How would you prevent double instantiation?” (Singleton with locking)
Don’t over-engineer! Use patterns when they solve real problems, not to show off. Simple, readable code is often better than pattern-heavy code. Ask yourself: “Would this be simpler without the pattern?”

Interview Deep-Dive Questions

Strong Answer:
  • The if/elif chain works fine for 4 channels today, but the real question is: how often will this list change? If the answer is “every quarter when marketing adds a new channel,” then every change forces you to modify the dispatching function, re-test it, and risk breaking existing channels. That violates the Open/Closed Principle — the code is not closed for modification.
  • I would use the Strategy pattern here. Define a NotificationChannel interface with a send(message, recipient) method. Each channel (Email, SMS, Push, Slack) is a concrete strategy. The calling code receives the strategy via dependency injection or a factory, and calls strategy.send() without knowing or caring which channel it is.
  • The factory that maps "email" to EmailChannel is the only place that needs updating when a new channel is added. The dispatching code, the business logic, and all existing channel implementations remain untouched.
  • However, I would not use this pattern if the system will only ever have 2-3 channels and the selection logic has domain-specific rules (e.g., “send SMS only if user is in the US and has opted in, otherwise fall back to email”). In that case, the if/elif chain captures the business logic more clearly than a pattern. Patterns should reduce complexity, not add indirection for its own sake.
  • The key insight for the interviewer: I am not choosing the pattern because “GoF says so.” I am choosing it because the axis of change (new channels being added frequently) aligns with what Strategy is designed to handle — isolating the varying behavior behind an interface.
Red flag answer: “I would use Strategy because it is best practice” without explaining why it fits this specific problem, or conversely, “if/elif is fine, patterns are over-engineering” without considering the rate of change and maintenance cost.Follow-ups:
  1. What if each notification channel requires completely different configuration (email needs SMTP settings, SMS needs a Twilio API key, push needs device tokens)? How do you handle the construction of these strategy objects — and does that suggest a second pattern?
  2. Now the product manager says “some notifications should go to multiple channels simultaneously.” How does your design change? Does Strategy still work, or do you need Observer/Chain of Responsibility?
Strong Answer:
  • You are right that the UML is nearly identical: both have a Context that delegates to an interface, with multiple concrete implementations. The difference is intent and who controls transitions.
  • Strategy: The client chooses which algorithm to inject. The context does not change its strategy on its own. Example: a payment system where the user picks credit card vs PayPal at checkout. The ShoppingCart does not spontaneously switch from CreditCard to PayPal — the user (external actor) makes that choice.
  • State: The object itself transitions between states based on internal logic. The context’s behavior changes over time as its state changes. Example: an order that moves from Pending to Processing to Shipped to Delivered. The order transitions itself — no external actor says “now be in Shipped state.” Each state class knows which state comes next and triggers the transition.
  • The practical test: if you see context.set_strategy(new_strategy) called by client code, it is Strategy. If you see self.context.state = NextState() called from within a state class, it is State.
  • Another heuristic: Strategy objects tend to be stateless (they are pure algorithms), while State objects often carry state-specific data and have transition logic. In a State pattern, the state classes form a finite state machine with defined transitions; in Strategy, the strategies are independent and do not know about each other.
  • A real-world example that blurs the line: a rate limiter that switches between AllowAll, Throttle, and BlockAll strategies based on current load. Is this Strategy (swapping algorithms) or State (the system transitions between states)? If the system itself monitors load and transitions automatically, it is State. If an ops engineer manually flips a switch, it is Strategy. Same structure, different intent.
Red flag answer: “They are basically the same thing, just different names” or only explaining one of them. The candidate should be able to articulate the intent difference clearly, because this question directly tests whether they understand patterns at a conceptual level or just memorized the UML.Follow-ups:
  1. Can you give me an example of a system where you might start with Strategy and later realize it should be State (or vice versa)? What would trigger that refactoring?
  2. In the State pattern implementation shown earlier, each state class creates the next state object directly (e.g., order.state = ProcessingState()). What is the problem with this coupling, and how would you fix it?
Strong Answer:
  • Singleton for everything “shared”: Teams often reach for Singleton for any class they want globally accessible — configuration, logging, feature flags, API clients. The problem: Singleton hides dependencies. A function that internally accesses DatabaseConnection.get_instance() has an invisible dependency on the database — its signature does not reveal this. Unit testing becomes painful because you cannot substitute a mock without monkey-patching global state. In one project I worked on, 14 Singletons created a hidden dependency graph that made the startup order brittle — service A’s singleton initialized before service B’s was ready, causing intermittent failures. The fix: dependency injection. Pass the database connection as a constructor argument. It is more explicit, more testable, and the “single instance” guarantee can be enforced at the composition root (the main function or DI container) rather than inside the class.
  • Observer pattern with too many subscribers and no backpressure: Observer is elegant for loose coupling, but in a high-throughput system, a subject notifying 200 observers synchronously on every state change turns a single write into 200 function calls on the same thread. If any observer is slow (e.g., one that writes to disk), it blocks the entire notification chain. I have seen this in event-driven systems where adding a new “just log this event” observer increased p99 latency by 300ms because the logger was doing synchronous I/O. The fix: either use async notification (message queue, event bus), or switch to a pub/sub system that decouples the producer’s latency from the consumers. The pattern itself does not tell you to think about backpressure — that is your job.
  • Factory pattern for a single concrete class: If NotificationFactory.create("email") always returns EmailNotification and there are no other notification types (and no realistic plan to add them), the factory is pure ceremony. It adds a layer of indirection, an extra file, and a string-based lookup that could fail at runtime — all for no benefit. This is the most common pattern anti-pattern: applying a pattern “just in case” against the YAGNI principle. Build the factory when the second or third concrete type actually materializes, not speculatively.
  • Bonus: Decorator stacks that are impossible to debug. When you have LoggingDecorator(RetryDecorator(TimeoutDecorator(AuthDecorator(HttpClient())))), a stack trace for an error is 8 layers deep, and understanding which decorator caught or transformed the exception requires reading every wrapper. At some point, a middleware pipeline (like Django/Express middleware) is clearer because it has explicit ordering and standardized hooks.
Red flag answer: “There is no wrong time to use patterns, they are best practices.” This is the biggest red flag. Every pattern is a trade-off, and a senior engineer should be able to articulate the cost of each one. Another weak answer: vague statements like “when it is too complex” without concrete examples.Follow-ups:
  1. You are reviewing a pull request from a junior engineer who added a Factory, Strategy, and Observer to a feature that currently has one payment method and one notification channel. How do you give constructive feedback?
  2. How do you decide at what point to introduce a pattern? What heuristic or “rule of thumb” do you use to avoid both premature abstraction and too-late refactoring?
Strong Answer:
  • With inheritance, you create a new class for each combination of features. If you have a Coffee base class and want Milk, Sugar, and Whip as options, inheritance requires: MilkCoffee, SugarCoffee, WhipCoffee, MilkSugarCoffee, MilkWhipCoffee, SugarWhipCoffee, MilkSugarWhipCoffee — that is 2^N classes for N options. This is the “class explosion” problem. With Decorator, you have 3 decorator classes and compose them at runtime: Whip(Sugar(Milk(Coffee()))). Adding a 4th option (Caramel) adds 1 decorator class instead of doubling the class count.
  • Decorator is strictly better when: (a) you need to combine behaviors in arbitrary ways at runtime, (b) the set of behaviors grows independently, (c) you want to add/remove capabilities dynamically (e.g., toggling features via configuration).
  • Inheritance is the right call when: (a) the relationship is genuinely “is-a” and behavior is not composable (a Dog is an Animal, not a decorated Animal), (b) the subclass needs access to protected internals of the parent (decorators only access the public interface), (c) the hierarchy is shallow and stable — adding a subclass is cheaper than the decorator infrastructure.
  • A real-world example where Decorator shines: middleware in web frameworks. Each middleware (authentication, logging, rate limiting, compression) wraps the handler. You compose them in a chain, and you can reorder or disable them via configuration. Doing this with inheritance would be absurd.
  • A real-world example where inheritance is better: UI widget hierarchies (Button extends Widget). A Button is not a “decorated Widget” — it has fundamentally different rendering logic, not just added behavior on top of Widget’s rendering.
  • The gotcha: Decorator requires that all decorators and the base component share the same interface. If the base interface is large (20 methods), every decorator must delegate all 20 methods, which is tedious and error-prone. In Python, __getattr__ delegation can help, but in strongly typed languages like Java, it is boilerplate-heavy. This is where abstract decorator base classes (like CoffeeDecorator in the example) help — they provide default delegation so concrete decorators only override what they change.
Red flag answer: “Decorator is always better than inheritance because inheritance is bad.” This is cargo-cult reasoning. Inheritance is a tool; the question tests whether the candidate understands when each tool is appropriate, not whether they can recite “favor composition over inheritance.”Follow-ups:
  1. In Python, functools.wraps and function decorators (@decorator) look similar to the Decorator pattern but are actually different. What is the relationship, and can you use Python’s @decorator syntax to implement the GoF Decorator pattern?
  2. You have a Stream class with read() and write() methods. You want to add encryption, compression, and buffering as optional layers. Walk me through how you design this with the Decorator pattern and what the call flow looks like when a client calls read().
class OrderProcessor:
    def process(self, order):
        if order.status == "pending":
            self.validate(order)
            order.status = "validated"
        elif order.status == "validated":
            self.charge_payment(order)
            order.status = "paid"
        elif order.status == "paid":
            self.ship(order)
            order.status = "shipped"
        elif order.status == "shipped":
            self.deliver(order)
            order.status = "delivered"
Strong Answer:
  • This is a textbook candidate for the State pattern. The code uses string-based status checking and a growing if/elif chain to determine behavior. Every new state (e.g., “refunded”, “cancelled”, “returned”) adds another branch, and the logic for each state is tangled together in one method.
  • Problems with this code: (a) Open/Closed violation — adding a new state requires modifying process(), risking bugs in existing states. (b) No compile-time safetyorder.status is a string, so a typo like "valdiated" would silently fail. (c) State-specific logic is scattered — what if “validated” orders need additional behavior (e.g., sending a confirmation email)? You would add it to the elif branch, making the method longer and harder to test. (d) Transition rules are implicit — nothing prevents order.status from being set to “delivered” directly from “pending,” skipping validation and payment.
  • Refactoring to State pattern: Each state becomes a class (PendingState, ValidatedState, PaidState, ShippedState, DeliveredState). Each state class has a process(order) method that performs the state-specific action and transitions to the next state. The Order class holds a reference to its current state and delegates process() to it. Invalid transitions can raise exceptions in the state class (e.g., DeliveredState.process() raises “already delivered”).
  • However, I would push back on when to refactor: if this is a startup with 4 states that rarely change, this if/elif is readable and works. I would add a TODO and refactor when the 5th or 6th state appears. The pattern is the right long-term architecture, but premature extraction costs development time and adds indirection that a new team member must learn.
  • The string-based status is the more urgent fix regardless of the pattern question. At minimum, replace strings with an Enum (class OrderStatus(Enum): PENDING = "pending" ...) to get IDE autocomplete and catch typos.
Red flag answer: Either “this code is fine, it works” (does not see the maintenance problem) or “immediately refactor to State pattern” without considering whether the complexity is warranted yet. The best answer demonstrates judgment — knowing the right pattern AND knowing when to apply it.Follow-ups:
  1. After refactoring to the State pattern, how would you add a “cancelled” state that can be reached from Pending, Validated, or Paid (but not from Shipped or Delivered)? How does the State pattern make this easier or harder than the if/elif approach?
  2. What if this order processor needs to persist state to a database? How do you serialize and deserialize State pattern objects? What are the challenges?
Strong Answer:
  • In the textbook Observer, notify() iterates through observers and calls update() on each one synchronously, on the calling thread. This has three production problems: (a) Latency coupling — the slowest observer determines the total notification time. If 1 of 50 observers takes 500ms (e.g., writing to a slow external API), every state change takes at least 500ms. (b) Failure propagation — if an observer throws an exception, it can abort the notification loop, leaving the remaining observers un-notified (unless you wrap each call in try/catch). (c) Thread starvation — in a single-threaded event loop (like Node.js or a game loop), synchronous notification blocks the entire loop.
  • Kafka implements Observer at the infrastructure level: producers publish events to topics, consumers subscribe to topics. The crucial difference is asynchronous, persistent, buffered delivery. Producers do not wait for consumers. If a consumer is slow, events accumulate in the topic partition. Consumers can replay events, process at their own pace, and even go offline temporarily. This is the “Observer pattern at scale” — decoupled by a durable message broker.
  • React (state management via useState/useReducer): When state changes, React does not immediately re-render all subscribed components. It batches state updates, reconciles the virtual DOM, and only re-renders components whose props/state actually changed. This is asynchronous, batched notification with diffing — a highly optimized Observer. React 18’s concurrent mode takes this further by allowing React to interrupt and prioritize renders.
  • Django signals: post_save, pre_delete, etc. are synchronous Observer hooks. When you call model.save(), all connected signal handlers execute synchronously in the same database transaction. This is a common footgun: a signal handler that sends an HTTP request to a third-party API slows down every save. The Django community’s recommendation: use signals only for decoupled app-to-app communication within the same process; for anything involving I/O, dispatch to Celery (async task queue) from the signal handler.
  • The evolution: Textbook Observer (in-process, synchronous) to Event Bus (in-process, async) to Message Queue (cross-process, persistent) to Event Streaming (cross-service, replayable, ordered). Each step adds infrastructure complexity but gains in decoupling, resilience, and scalability.
Red flag answer: “Observer is just pub/sub” without explaining the synchronous vs asynchronous distinction, or not knowing that production systems almost never use the textbook synchronous Observer at any meaningful scale.Follow-ups:
  1. How would you handle the case where an observer needs the notification to be exactly-once (e.g., charging a credit card)? How does this change the Observer implementation?
  2. In the textbook Observer, if observer A detaches observer B during the notification loop (because A’s update logic removes B), what happens? How would you make the notification loop safe against concurrent modification?
Strong Answer:
  • Singleton has earned its “anti-pattern” reputation not because the pattern itself is flawed, but because it is dramatically overused for the wrong reasons. People use it for global access when what they actually need is single instance.
  • Testing nightmare: A class that calls Database.get_instance() internally cannot be tested with a mock database. You must monkey-patch the global singleton, which is fragile, order-dependent, and can bleed state between tests. In a test suite of 2000 tests, shared singleton state causes intermittent failures that are almost impossible to reproduce.
  • Hidden dependencies: When a function’s signature is process_order(order) but it internally accesses Config.instance(), Logger.instance(), and PaymentGateway.instance(), the function’s true dependencies are invisible. A new developer cannot understand what this function needs just by reading its signature. This is the antithesis of clean architecture.
  • Initialization ordering: If Singleton A depends on Singleton B during initialization, and B depends on A, you have a circular dependency that manifests as a crash or undefined behavior at startup. With dependency injection, the DI container detects this cycle at configuration time and gives you a clear error.
  • What to use instead: Dependency Injection (DI). Define the interface, create the instance once at the application’s composition root (the main() function or a DI container like Python’s dependency-injector), and pass it to everything that needs it. The result: same single instance in production, but tests can inject mocks, and every dependency is explicit in constructor signatures.
  • When Singleton is actually fine: Truly stateless utility classes (like a Math singleton — though that is better as a module), or when the language/framework provides singleton semantics naturally (Python modules are singletons, Spring beans are singletons by default). The pattern is fine; the abuse of it for everything “shared” is the problem.
  • A concrete example: A team had a FeatureFlags.instance() singleton that read flags from a file at startup. Unit tests could not test behavior with different flag combinations because the singleton loaded once and never reset. Switching to constructor injection (Service(feature_flags=flags)) let tests pass any flag configuration they wanted, and the production code still used a single FeatureFlags instance created at startup.
Red flag answer: “Singleton is an anti-pattern, never use it” (dogmatic) or “Singleton is fine, just use it for everything that should be shared” (no awareness of the problems). The nuanced answer is: the pattern is a tool, the overuse is the anti-pattern.Follow-ups:
  1. In Python specifically, how does module-level instantiation (config = Config() in config.py) differ from the Singleton pattern? What are the advantages and disadvantages?
  2. You inherit a codebase with 12 Singletons. You cannot rewrite everything at once. What is your incremental strategy to reduce Singleton dependency while keeping the system working?
Strong Answer:
  • I do not start by thinking about patterns. I start by identifying the axes of change — the parts of the system that are most likely to vary or grow over time. Patterns are solutions to specific change scenarios.
  • Axis 1 — Vehicle types (car, motorcycle, truck, EV): These share an interface (park(), getSize()) but have different behaviors. This is classic Factory territory. A VehicleFactory maps the vehicle type string from the entry sensor to a concrete class. When the lot adds EV charging spots, I add an ElectricVehicle class and update the factory — nothing else changes.
  • Axis 2 — Pricing strategies (hourly, daily, weekend, membership): The pricing logic varies by customer type and time. This is Strategy. A PricingStrategy interface with calculateFee(entry_time, exit_time). The ParkingTicket holds a reference to the pricing strategy. When marketing adds a “holiday pricing” tier, it is a new strategy class — no modification to existing pricing logic.
  • Axis 3 — Spot assignment logic: How do you pick which spot to assign? Closest to entrance? Closest to exit? Distribute evenly across floors? This varies by lot and could even change at runtime (normal mode vs event mode). Again Strategy, but for a different axis. The ParkingLot delegates spot assignment to a SpotAssignmentStrategy.
  • Axis 4 — Notifications (spot available, lot full, payment receipt): Multiple systems need to react to events (display boards, mobile app, admin dashboard). This is Observer — the ParkingLot is the subject, displays and apps are observers.
  • Axis 5 — Payment processing: Multiple payment methods. Strategy again — PaymentStrategy interface with CreditCard, Cash, MobilePayment implementations.
  • What I would not use: Singleton for the ParkingLot class (a common mistake). There is nothing preventing a company from managing multiple lots. Even if there is “only one lot,” making it a Singleton adds global state. I would create one instance and pass it through constructor injection.
  • My reasoning framework: for each entity or behavior, I ask: (a) Will there be multiple variants? If yes, Factory or Strategy. (b) Does behavior change based on state? State pattern. (c) Do multiple components need to react to changes? Observer. (d) Is the construction complex? Builder. If the answer is “no” to all of these, plain classes and simple methods are fine — no pattern needed.
Red flag answer: “I would use Singleton for ParkingLot, Factory for Vehicles, Strategy for Payment, Observer for notifications” — listing patterns without explaining why each one fits. The interviewer is testing the reasoning process, not pattern name-dropping. An even worse answer: starting with “let me apply the 23 GoF patterns” — this shows the candidate is pattern-driven rather than problem-driven.Follow-ups:
  1. The interviewer pushes back: “You used Strategy for both pricing and spot assignment. Is that not confusing? How do you keep the codebase clear when the same pattern appears multiple times?” How do you respond?
  2. Six months after launch, the parking lot needs to support “reserved spots” that are pre-booked by time slot. Which existing pattern in your design accommodates this, or do you need a new one?

🔗 Continue Learning

Practice: Case Studies

See patterns in action with real problems

UML Diagrams

Learn to draw pattern diagrams

Interview Cheat Sheet

Quick reference for interviews