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. There are 23 classic βGang of Fourβ patterns divided into 3 categories:
Creational (5) How objects are created Singleton, Factory, Abstract Factory, Builder, Prototype
Structural (7) How objects are composed Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
Behavioral (11) How objects communicate Strategy, 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.
Thread-Safe
Python Module
Metaclass
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
# config.py - Python modules are naturally singleton
class _Config :
def __init__ ( self ):
self .debug = False
self .database_url = "localhost:5432"
def load_from_env ( self ):
import os
self .debug = os.getenv( "DEBUG" , "false" ) == "true"
config = _Config() # Single instance created on import
# Usage in other files
from config import config
print (config.debug)
class SingletonMeta ( type ):
_instances = {}
_lock = threading.Lock()
def __call__ ( cls , * args , ** kwargs ):
with cls ._lock:
if cls not in cls ._instances:
instance = super (). __call__ ( * args, ** kwargs)
cls ._instances[ cls ] = instance
return cls ._instances[ cls ]
class Logger ( metaclass = SingletonMeta ):
def log ( self , message ):
print ( f "[LOG] { message } " )
# All these return the same instance
logger1 = Logger()
logger2 = Logger()
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.
from abc import ABC , abstractmethod
# Product interface
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.
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 family of interchangeable algorithms.
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.
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.
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
Problem Pattern Need single instance Singleton Create objects without knowing class Factory Complex object construction Builder Integrate incompatible interfaces Adapter Add features dynamically Decorator Simplify complex subsystem Facade Interchangeable algorithms Strategy Notify of state changes Observer Behavior changes with state State
π₯ Top Patterns for Interviews
These patterns appear most frequently in LLD interviews:
Must-Know (80% of interviews)
Pattern Common Use Cases Factory Vehicle types, Payment methods, Notification channels Strategy Payment processing, Shipping calculation, Pricing rules Observer Stock price alerts, Order status, Pub/Sub systems State Order lifecycle, Elevator, Vending machine, ATM Singleton Database, Configuration, Logger
Good to Know (20% of interviews)
Pattern Common Use Cases Builder Complex configurations, Query builders Decorator Middleware, Permission layers Adapter Legacy integration, Third-party APIs Command Undo/Redo, Transaction logs Facade Complex 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
Pattern Intent Use When Avoid When Singleton One instance globally DB connection, Config Need testability, multiple instances Factory Create objects without specifying class Multiple product types Only one concrete class Builder Complex object construction Many optional parameters Simple objects Strategy Swap algorithms at runtime Multiple ways to do something Only one algorithm Observer Notify subscribers of changes Decoupled event handling Few, known subscribers State Object behavior changes with state Finite state machine Simple conditionals suffice Decorator Add behavior dynamically Layered functionality Inheritance works fine Facade Simplify complex subsystems Complex library integration Simple interfaces already Command Encapsulate requests as objects Undo/redo, queuing Simple 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: Pattern Benefit Cost Singleton Global access, one instance Hard to test, hidden dependencies Factory Decoupled creation More abstraction layers Observer Loose coupling Memory leaks if not detached Strategy Swappable algorithms More classes to manage Decorator Flexible composition Complex 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
Common Interview Questions
β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?β
π Continue Learning