Home / Notebooks / Software Architecture
Software Architecture
intermediate

Design Patterns Essentials

Complete guide to software design patterns with practical examples

April 20, 2026
Updated regularly

Design Patterns Essentials

Complete guide to essential software design patterns with practical examples in multiple languages.

What are Design Patterns?

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

Key Benefits:

  • Proven solutions to recurring problems
  • Common vocabulary for developers
  • Improved code maintainability
  • Faster development
  • Better architecture decisions
  • Pattern Categories:

  • Creational: Object creation mechanisms
  • Structural: Object composition and relationships
  • Behavioral: Object interaction and responsibility
  • Creational Patterns

    Singleton Pattern

    Purpose: Ensure a class has only one instance and provide global access to it.

    Use Cases:

  • Database connections
  • Configuration managers
  • Logging services
  • Cache managers
  • Implementation:

    # ========== Python Implementation ==========
    class DatabaseConnection:
        """Singleton database connection"""
        _instance = None
        
        def __new__(cls):
            if cls._instance is None:
                cls._instance = super().__new__(cls)
                cls._instance._initialize()
            return cls._instance
        
        def _initialize(self):
            """Initialize connection (only called once)"""
            self.connection = "Database connected"
            print("Database connection established")
        
        def query(self, sql):
            """Execute query"""
            return f"Executing: {sql}"
    
    # Usage
    db1 = DatabaseConnection()
    db2 = DatabaseConnection()
    
    print(db1 is db2)  # True - same instance
    print(db1.query("SELECT * FROM users"))
    
    // ========== JavaScript Implementation ==========
    class ConfigManager {
        constructor() {
            if (ConfigManager.instance) {
                return ConfigManager.instance;
            }
            
            this.config = {};
            ConfigManager.instance = this;
        }
        
        set(key, value) {
            this.config[key] = value;
        }
        
        get(key) {
            return this.config[key];
        }
    }
    
    // Usage
    const config1 = new ConfigManager();
    const config2 = new ConfigManager();
    
    console.log(config1 === config2);  // true
    config1.set('apiUrl', 'https://api.example.com');
    console.log(config2.get('apiUrl'));  // https://api.example.com
    

    When to Use:

  • ✅ Need exactly one instance (logging, config)
  • ✅ Global point of access required
  • ❌ Avoid for simple data sharing (use dependency injection)
  • Factory Pattern

    Purpose: Create objects without specifying exact class to create.

    Use Cases:

  • Creating different types of objects based on input
  • Plugin systems
  • Document creators (PDF, Word, etc.)
  • Implementation:

    # ========== Abstract Product ==========
    from abc import ABC, abstractmethod
    
    class Transport(ABC):
        """Abstract transport interface"""
        @abstractmethod
        def deliver(self):
            pass
    
    # ========== Concrete Products ==========
    class Truck(Transport):
        def deliver(self):
            return "Delivering by land in a truck"
    
    class Ship(Transport):
        def deliver(self):
            return "Delivering by sea in a ship"
    
    class Airplane(Transport):
        def deliver(self):
            return "Delivering by air in an airplane"
    
    # ========== Factory ==========
    class TransportFactory:
        """Factory to create transport objects"""
        
        @staticmethod
        def create_transport(transport_type: str) -> Transport:
            """
            Create transport based on type
            
            Args:
                transport_type: 'truck', 'ship', or 'airplane'
            
            Returns:
                Transport object
            """
            transports = {
                'truck': Truck,
                'ship': Ship,
                'airplane': Airplane
            }
            
            transport_class = transports.get(transport_type.lower())
            if not transport_class:
                raise ValueError(f"Unknown transport type: {transport_type}")
            
            return transport_class()
    
    # ========== Usage ==========
    # Create transports without knowing specific classes
    truck = TransportFactory.create_transport('truck')
    ship = TransportFactory.create_transport('ship')
    airplane = TransportFactory.create_transport('airplane')
    
    print(truck.deliver())      # Delivering by land in a truck
    print(ship.deliver())        # Delivering by sea in a ship
    print(airplane.deliver())    # Delivering by air in an airplane
    

    When to Use:

  • ✅ Don't know exact types at compile time
  • ✅ Want to centralize object creation
  • ✅ Need to extend with new types easily
  • Builder Pattern

    Purpose: Construct complex objects step by step.

    Use Cases:

  • Creating objects with many optional parameters
  • Building complex configurations
  • Generating reports/documents
  • Implementation:

    # ========== Product ==========
    class Computer:
        """Complex product with many parts"""
        def __init__(self):
            self.cpu = None
            self.ram = None
            self.storage = None
            self.gpu = None
            self.os = None
        
        def __str__(self):
            return (f"Computer: CPU={self.cpu}, RAM={self.ram}GB, "
                    f"Storage={self.storage}, GPU={self.gpu}, OS={self.os}")
    
    # ========== Builder ==========
    class ComputerBuilder:
        """Builder for constructing Computer objects"""
        
        def __init__(self):
            self.computer = Computer()
        
        def set_cpu(self, cpu):
            """Set CPU (required)"""
            self.computer.cpu = cpu
            return self  # Return self for method chaining
        
        def set_ram(self, ram):
            """Set RAM in GB"""
            self.computer.ram = ram
            return self
        
        def set_storage(self, storage):
            """Set storage type and size"""
            self.computer.storage = storage
            return self
        
        def set_gpu(self, gpu):
            """Set GPU (optional)"""
            self.computer.gpu = gpu
            return self
        
        def set_os(self, os):
            """Set operating system"""
            self.computer.os = os
            return self
        
        def build(self):
            """Return the constructed computer"""
            # Validate required fields
            if not self.computer.cpu or not self.computer.ram:
                raise ValueError("CPU and RAM are required")
            return self.computer
    
    # ========== Usage ==========
    # Build a gaming computer
    gaming_pc = (ComputerBuilder()
        .set_cpu("Intel i9")
        .set_ram(32)
        .set_storage("1TB NVMe SSD")
        .set_gpu("RTX 4090")
        .set_os("Windows 11")
        .build())
    
    print(gaming_pc)
    
    # Build a basic office computer
    office_pc = (ComputerBuilder()
        .set_cpu("Intel i5")
        .set_ram(16)
        .set_storage("512GB SSD")
        .set_os("Windows 11")
        .build())
    
    print(office_pc)
    

    When to Use:

  • ✅ Object has many optional parameters
  • ✅ Step-by-step construction needed
  • ✅ Want immutable objects
  • Structural Patterns

    Adapter Pattern

    Purpose: Convert interface of a class into another interface clients expect.

    Use Cases:

  • Integrating third-party libraries
  • Legacy code integration
  • API compatibility layers
  • Implementation:

    # ========== Existing Interface (Target) ==========
    class MediaPlayer:
        """Interface that client expects"""
        def play(self, audio_type, filename):
            pass
    
    # ========== Incompatible Interface (Adaptee) ==========
    class AdvancedMediaPlayer:
        """Third-party player with different interface"""
        def play_mp4(self, filename):
            print(f"Playing MP4 file: {filename}")
        
        def play_mkv(self, filename):
            print(f"Playing MKV file: {filename}")
    
    # ========== Adapter ==========
    class MediaAdapter(MediaPlayer):
        """Adapter to make AdvancedMediaPlayer compatible"""
        
        def __init__(self):
            self.advanced_player = AdvancedMediaPlayer()
        
        def play(self, audio_type, filename):
            """Convert interface"""
            if audio_type == 'mp4':
                self.advanced_player.play_mp4(filename)
            elif audio_type == 'mkv':
                self.advanced_player.play_mkv(filename)
            else:
                print(f"Unsupported format: {audio_type}")
    
    # ========== Client ==========
    class AudioPlayer(MediaPlayer):
        """Client using the adapter"""
        
        def __init__(self):
            self.media_adapter = None
        
        def play(self, audio_type, filename):
            # Built-in support for mp3
            if audio_type == 'mp3':
                print(f"Playing MP3 file: {filename}")
            # Use adapter for other formats
            elif audio_type in ['mp4', 'mkv']:
                self.media_adapter = MediaAdapter()
                self.media_adapter.play(audio_type, filename)
            else:
                print(f"Invalid format: {audio_type}")
    
    # ========== Usage ==========
    player = AudioPlayer()
    player.play('mp3', 'song.mp3')      # Playing MP3 file: song.mp3
    player.play('mp4', 'video.mp4')     # Playing MP4 file: video.mp4
    player.play('mkv', 'movie.mkv')     # Playing MKV file: movie.mkv
    

    When to Use:

  • ✅ Use existing class with incompatible interface
  • ✅ Create reusable class with unrelated classes
  • ✅ Integrate third-party libraries
  • Decorator Pattern

    Purpose: Add new functionality to objects dynamically.

    Use Cases:

  • Adding features to objects at runtime
  • Following Single Responsibility Principle
  • Extending functionality without inheritance
  • Implementation:

    # ========== Component Interface ==========
    from abc import ABC, abstractmethod
    
    class Coffee(ABC):
        """Base coffee interface"""
        @abstractmethod
        def cost(self):
            pass
        
        @abstractmethod
        def description(self):
            pass
    
    # ========== Concrete Component ==========
    class SimpleCoffee(Coffee):
        """Basic coffee"""
        def cost(self):
            return 5.0
        
        def description(self):
            return "Simple Coffee"
    
    # ========== Decorator Base ==========
    class CoffeeDecorator(Coffee):
        """Base decorator"""
        def __init__(self, coffee: Coffee):
            self._coffee = coffee
        
        def cost(self):
            return self._coffee.cost()
        
        def description(self):
            return self._coffee.description()
    
    # ========== Concrete Decorators ==========
    class MilkDecorator(CoffeeDecorator):
        """Add milk"""
        def cost(self):
            return self._coffee.cost() + 1.5
        
        def description(self):
            return self._coffee.description() + ", Milk"
    
    class SugarDecorator(CoffeeDecorator):
        """Add sugar"""
        def cost(self):
            return self._coffee.cost() + 0.5
        
        def description(self):
            return self._coffee.description() + ", Sugar"
    
    class WhipDecorator(CoffeeDecorator):
        """Add whipped cream"""
        def cost(self):
            return self._coffee.cost() + 2.0
        
        def description(self):
            return self._coffee.description() + ", Whipped Cream"
    
    # ========== Usage ==========
    # Start with simple coffee
    coffee = SimpleCoffee()
    print(f"{coffee.description()}: ${coffee.cost()}")
    # Output: Simple Coffee: $5.0
    
    # Add milk
    coffee = MilkDecorator(coffee)
    print(f"{coffee.description()}: ${coffee.cost()}")
    # Output: Simple Coffee, Milk: $6.5
    
    # Add sugar
    coffee = SugarDecorator(coffee)
    print(f"{coffee.description()}: ${coffee.cost()}")
    # Output: Simple Coffee, Milk, Sugar: $7.0
    
    # Add whipped cream
    coffee = WhipDecorator(coffee)
    print(f"{coffee.description()}: ${coffee.cost()}")
    # Output: Simple Coffee, Milk, Sugar, Whipped Cream: $9.0
    
    # Create fancy coffee in one go
    fancy_coffee = WhipDecorator(
        SugarDecorator(
            MilkDecorator(
                SimpleCoffee()
            )
        )
    )
    print(f"{fancy_coffee.description()}: ${fancy_coffee.cost()}")
    

    When to Use:

  • ✅ Add responsibilities without affecting other objects
  • ✅ Avoid class explosion from inheritance
  • ✅ Combine behaviors dynamically
  • Facade Pattern

    Purpose: Provide simplified interface to complex subsystem.

    Use Cases:

  • Simplifying complex libraries
  • Creating API wrappers
  • Hiding implementation details
  • Implementation:

    # ========== Complex Subsystem ==========
    class CPU:
        """Complex component"""
        def freeze(self):
            print("CPU: Freezing...")
        
        def jump(self, position):
            print(f"CPU: Jumping to {position}")
        
        def execute(self):
            print("CPU: Executing...")
    
    class Memory:
        """Complex component"""
        def load(self, position, data):
            print(f"Memory: Loading {data} at {position}")
    
    class HardDrive:
        """Complex component"""
        def read(self, sector, size):
            print(f"HardDrive: Reading {size} bytes from sector {sector}")
            return "boot_data"
    
    # ========== Facade ==========
    class ComputerFacade:
        """
        Simple interface to complex computer subsystem
        Hides complexity of CPU, Memory, and HardDrive
        """
        
        def __init__(self):
            self.cpu = CPU()
            self.memory = Memory()
            self.hard_drive = HardDrive()
        
        def start(self):
            """
            Simple one-method interface to start computer
            Internally coordinates complex boot sequence
            """
            print("=== Starting Computer ===")
            self.cpu.freeze()
            boot_data = self.hard_drive.read(sector=0, size=1024)
            self.memory.load(position=0x00, data=boot_data)
            self.cpu.jump(position=0x00)
            self.cpu.execute()
            print("=== Computer Started ===\n")
    
    # ========== Usage ==========
    # Without Facade (complex)
    # cpu = CPU()
    # memory = Memory()
    # hdd = HardDrive()
    # cpu.freeze()
    # boot_data = hdd.read(0, 1024)
    # memory.load(0x00, boot_data)
    # cpu.jump(0x00)
    # cpu.execute()
    
    # With Facade (simple!)
    computer = ComputerFacade()
    computer.start()  # One simple call!
    

    When to Use:

  • ✅ Simplify complex subsystem
  • ✅ Layer your architecture
  • ✅ Reduce dependencies on subsystem
  • Behavioral Patterns

    Observer Pattern

    Purpose: Define one-to-many dependency between objects.

    Use Cases:

  • Event handling systems
  • MVC architecture
  • Real-time notifications
  • Pub/Sub systems
  • Implementation:

    # ========== Subject (Observable) ==========
    class Subject:
        """Observable object that notifies observers"""
        
        def __init__(self):
            self._observers = []
            self._state = None
        
        def attach(self, observer):
            """Add observer"""
            if observer not in self._observers:
                self._observers.append(observer)
                print(f"Attached: {observer.__class__.__name__}")
        
        def detach(self, observer):
            """Remove observer"""
            self._observers.remove(observer)
            print(f"Detached: {observer.__class__.__name__}")
        
        def notify(self):
            """Notify all observers of state change"""
            print("Notifying observers...")
            for observer in self._observers:
                observer.update(self)
        
        @property
        def state(self):
            return self._state
        
        @state.setter
        def state(self, value):
            """When state changes, notify observers"""
            print(f"State changed to: {value}")
            self._state = value
            self.notify()
    
    # ========== Observer Interface ==========
    class Observer:
        """Observer that reacts to subject changes"""
        def update(self, subject):
            pass
    
    # ========== Concrete Observers ==========
    class EmailNotifier(Observer):
        """Observer that sends email notifications"""
        def update(self, subject):
            print(f"  📧 Email: Sending notification - State is {subject.state}")
    
    class SMSNotifier(Observer):
        """Observer that sends SMS notifications"""
        def update(self, subject):
            print(f"  📱 SMS: Sending text - State is {subject.state}")
    
    class LoggerObserver(Observer):
        """Observer that logs state changes"""
        def update(self, subject):
            print(f"  📝 Logger: Recording state change to {subject.state}")
    
    # ========== Usage ==========
    # Create subject
    stock_price = Subject()
    
    # Create observers
    email = EmailNotifier()
    sms = SMSNotifier()
    logger = LoggerObserver()
    
    # Subscribe observers
    stock_price.attach(email)
    stock_price.attach(sms)
    stock_price.attach(logger)
    
    # Change state - all observers notified
    stock_price.state = 100
    # Output:
    # State changed to: 100
    # Notifying observers...
    #   📧 Email: Sending notification - State is 100
    #   📱 SMS: Sending text - State is 100
    #   📝 Logger: Recording state change to 100
    
    print()
    
    # Change state again
    stock_price.state = 150
    
    print()
    
    # Unsubscribe one observer
    stock_price.detach(sms)
    
    # Change state - only remaining observers notified
    stock_price.state = 200
    

    When to Use:

  • ✅ One object change affects unknown number of others
  • ✅ Loosely coupled communication
  • ✅ Event-driven architecture
  • Strategy Pattern

    Purpose: Define family of algorithms, encapsulate each, make them interchangeable.

    Use Cases:

  • Different ways to perform same task
  • Payment methods
  • Sorting algorithms
  • Compression algorithms
  • Implementation:

    # ========== Strategy Interface ==========
    from abc import ABC, abstractmethod
    
    class PaymentStrategy(ABC):
        """Abstract payment strategy"""
        @abstractmethod
        def pay(self, amount):
            pass
    
    # ========== Concrete Strategies ==========
    class CreditCardPayment(PaymentStrategy):
        """Pay with credit card"""
        def __init__(self, card_number, cvv):
            self.card_number = card_number
            self.cvv = cvv
        
        def pay(self, amount):
            print(f"Paid ${amount} using Credit Card ending in {self.card_number[-4:]}")
    
    class PayPalPayment(PaymentStrategy):
        """Pay with PayPal"""
        def __init__(self, email):
            self.email = email
        
        def pay(self, amount):
            print(f"Paid ${amount} using PayPal account {self.email}")
    
    class CryptoPayment(PaymentStrategy):
        """Pay with cryptocurrency"""
        def __init__(self, wallet_address):
            self.wallet_address = wallet_address
        
        def pay(self, amount):
            print(f"Paid ${amount} using Crypto wallet {self.wallet_address[:10]}...")
    
    # ========== Context ==========
    class ShoppingCart:
        """Context that uses payment strategy"""
        
        def __init__(self):
            self.items = []
            self.payment_strategy = None
        
        def add_item(self, item, price):
            """Add item to cart"""
            self.items.append({'item': item, 'price': price})
        
        def calculate_total(self):
            """Calculate total price"""
            return sum(item['price'] for item in self.items)
        
        def set_payment_strategy(self, strategy: PaymentStrategy):
            """Set payment method (strategy)"""
            self.payment_strategy = strategy
        
        def checkout(self):
            """Process payment using selected strategy"""
            if not self.payment_strategy:
                print("Please select a payment method")
                return
            
            total = self.calculate_total()
            print(f"Total: ${total}")
            self.payment_strategy.pay(total)
    
    # ========== Usage ==========
    # Create shopping cart
    cart = ShoppingCart()
    cart.add_item("Laptop", 999)
    cart.add_item("Mouse", 25)
    cart.add_item("Keyboard", 75)
    
    print("=== Checkout with Credit Card ===")
    cart.set_payment_strategy(CreditCardPayment("1234567890123456", "123"))
    cart.checkout()
    # Output:
    # Total: $1099
    # Paid $1099 using Credit Card ending in 3456
    
    print("\n=== Checkout with PayPal ===")
    cart.set_payment_strategy(PayPalPayment("user@example.com"))
    cart.checkout()
    # Output:
    # Total: $1099
    # Paid $1099 using PayPal account user@example.com
    
    print("\n=== Checkout with Crypto ===")
    cart.set_payment_strategy(CryptoPayment("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"))
    cart.checkout()
    # Output:
    # Total: $1099
    # Paid $1099 using Crypto wallet 0x742d35Cc...
    

    When to Use:

  • ✅ Multiple algorithms for same task
  • ✅ Avoid conditional statements
  • ✅ Algorithm may change at runtime
  • Command Pattern

    Purpose: Encapsulate request as an object.

    Use Cases:

  • Undo/redo functionality
  • Transaction systems
  • Task queues
  • Macro recording
  • Implementation:

    # ========== Command Interface ==========
    from abc import ABC, abstractmethod
    
    class Command(ABC):
        """Abstract command"""
        @abstractmethod
        def execute(self):
            pass
        
        @abstractmethod
        def undo(self):
            pass
    
    # ========== Receiver ==========
    class Light:
        """Receiver - actual object that performs action"""
        def __init__(self, location):
            self.location = location
            self.is_on = False
        
        def turn_on(self):
            self.is_on = True
            print(f"{self.location} light is ON")
        
        def turn_off(self):
            self.is_on = False
            print(f"{self.location} light is OFF")
    
    # ========== Concrete Commands ==========
    class LightOnCommand(Command):
        """Command to turn light on"""
        def __init__(self, light: Light):
            self.light = light
        
        def execute(self):
            self.light.turn_on()
        
        def undo(self):
            self.light.turn_off()
    
    class LightOffCommand(Command):
        """Command to turn light off"""
        def __init__(self, light: Light):
            self.light = light
        
        def execute(self):
            self.light.turn_off()
        
        def undo(self):
            self.light.turn_on()
    
    # ========== Invoker ==========
    class RemoteControl:
        """Invoker - triggers commands"""
        def __init__(self):
            self.commands = {}
            self.history = []
        
        def set_command(self, button, command: Command):
            """Assign command to button"""
            self.commands[button] = command
        
        def press_button(self, button):
            """Execute command"""
            if button in self.commands:
                command = self.commands[button]
                command.execute()
                self.history.append(command)
            else:
                print(f"No command assigned to button {button}")
        
        def undo(self):
            """Undo last command"""
            if self.history:
                command = self.history.pop()
                command.undo()
            else:
                print("Nothing to undo")
    
    # ========== Usage ==========
    # Create receivers
    living_room_light = Light("Living Room")
    bedroom_light = Light("Bedroom")
    
    # Create commands
    living_room_on = LightOnCommand(living_room_light)
    living_room_off = LightOffCommand(living_room_light)
    bedroom_on = LightOnCommand(bedroom_light)
    bedroom_off = LightOffCommand(bedroom_light)
    
    # Create invoker (remote control)
    remote = RemoteControl()
    
    # Assign commands to buttons
    remote.set_command('1', living_room_on)
    remote.set_command('2', living_room_off)
    remote.set_command('3', bedroom_on)
    remote.set_command('4', bedroom_off)
    
    # Use remote
    print("=== Using Remote Control ===")
    remote.press_button('1')  # Living Room light is ON
    remote.press_button('3')  # Bedroom light is ON
    remote.press_button('2')  # Living Room light is OFF
    
    print("\n=== Undo Operations ===")
    remote.undo()  # Living Room light is ON (undo last OFF)
    remote.undo()  # Bedroom light is OFF (undo bedroom ON)
    remote.undo()  # Living Room light is OFF (undo living room ON)
    

    When to Use:

  • ✅ Parameterize objects with operations
  • ✅ Queue operations
  • ✅ Support undo/redo
  • ✅ Log changes
  • Real-World Examples

    Example 1: E-commerce System

    Combining multiple patterns:

    # ========== Singleton: Configuration ==========
    class Config:
        _instance = None
        
        def __new__(cls):
            if cls._instance is None:
                cls._instance = super().__new__(cls)
                cls._instance.settings = {
                    'tax_rate': 0.08,
                    'shipping_cost': 10.0
                }
            return cls._instance
    
    # ========== Factory: Product Creation ==========
    class Product:
        def __init__(self, name, price):
            self.name = name
            self.price = price
    
    class ProductFactory:
        @staticmethod
        def create_product(product_type, name, price):
            products = {
                'physical': lambda: PhysicalProduct(name, price),
                'digital': lambda: DigitalProduct(name, price)
            }
            return products[product_type]()
    
    class PhysicalProduct(Product):
        def calculate_shipping(self):
            return Config().settings['shipping_cost']
    
    class DigitalProduct(Product):
        def calculate_shipping(self):
            return 0  # No shipping for digital products
    
    # ========== Strategy: Discount Calculation ==========
    class DiscountStrategy(ABC):
        @abstractmethod
        def calculate(self, amount):
            pass
    
    class NoDiscount(DiscountStrategy):
        def calculate(self, amount):
            return amount
    
    class PercentageDiscount(DiscountStrategy):
        def __init__(self, percentage):
            self.percentage = percentage
        
        def calculate(self, amount):
            return amount * (1 - self.percentage / 100)
    
    # ========== Observer: Order Notifications ==========
    class Order:
        def __init__(self):
            self._observers = []
            self._status = 'pending'
        
        def attach(self, observer):
            self._observers.append(observer)
        
        def set_status(self, status):
            self._status = status
            for observer in self._observers:
                observer.update(status)
    
    class EmailNotification:
        def update(self, status):
            print(f"Email: Order status changed to {status}")
    
    # ========== Complete Flow ==========
    # Create products
    laptop = ProductFactory.create_product('physical', 'Laptop', 1000)
    ebook = ProductFactory.create_product('digital', 'E-Book', 20)
    
    # Apply discount
    discount = PercentageDiscount(10)
    final_price = discount.calculate(laptop.price)
    
    # Create order and attach observer
    order = Order()
    order.attach(EmailNotification())
    order.set_status('confirmed')  # Email: Order status changed to confirmed
    

    Example 2: Logging System

    # ========== Singleton: Logger ==========
    class Logger:
        _instance = None
        
        def __new__(cls):
            if cls._instance is None:
                cls._instance = super().__new__(cls)
                cls._instance.logs = []
            return cls._instance
        
        def log(self, message, level='INFO'):
            log_entry = f"[{level}] {message}"
            self.logs.append(log_entry)
            print(log_entry)
    
    # ========== Decorator: Log Formatting ==========
    class LogDecorator:
        def __init__(self, logger):
            self.logger = logger
        
        def log(self, message, level='INFO'):
            self.logger.log(message, level)
    
    class TimestampDecorator(LogDecorator):
        def log(self, message, level='INFO'):
            from datetime import datetime
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            message = f"{timestamp} - {message}"
            self.logger.log(message, level)
    
    # Usage
    logger = Logger()
    decorated_logger = TimestampDecorator(logger)
    decorated_logger.log("Application started")
    # Output: [INFO] 2024-04-20 10:30:00 - Application started
    

    Best Practices

    When to Use Design Patterns

    ✅ DO Use When:

  • Problem fits pattern perfectly
  • Team understands the pattern
  • Benefits outweigh complexity
  • Code will be maintained long-term
  • ❌ DON'T Use When:

  • Forcing pattern where it doesn't fit
  • Over-engineering simple problems
  • Team unfamiliar with patterns
  • Quick prototype/proof of concept
  • Common Pitfalls

    1. Pattern Overuse
    # ❌ Bad: Using pattern for simple task
    class SingletonStringFormatter:
        _instance = None
        def format(self, s):
            return s.upper()
    
    # ✅ Good: Simple function
    def format_string(s):
        return s.upper()
    
    2. Ignoring Language Features
    # ❌ Bad: Implementing Singleton in Python
    class Singleton:
        _instance = None
        def __new__(cls):
            if cls._instance is None:
                cls._instance = super().__new__(cls)
            return cls._instance
    
    # ✅ Good: Use module (Python modules are singletons)
    # config.py
    settings = {'key': 'value'}
    
    # Import and use
    from config import settings
    

    3. Not Considering Alternatives

  • Consider composition over inheritance
  • Use dependency injection instead of Singleton
  • Use higher-order functions instead of Strategy in functional languages
  • Pattern Selection Guide

    By Problem Type

    Object Creation:

  • Many optional parameters → Builder
  • One instance needed → Singleton
  • Create family of objects → Factory
  • Object Structure:

  • Incompatible interfaces → Adapter
  • Add features dynamically → Decorator
  • Simplify complex system → Facade
  • Object Behavior:

  • Notify multiple objects → Observer
  • Swap algorithms → Strategy
  • Encapsulate requests → Command
  • Resources

  • Books:
  • - "Design Patterns" by Gang of Four - "Head First Design Patterns" - "Patterns of Enterprise Application Architecture"
  • Online:
  • - Refactoring Guru: https://refactoring.guru/design-patterns - Source Making: https://sourcemaking.com/design_patterns
  • Practice:
  • - Implement patterns in your language - Refactor existing code using patterns - Code reviews focusing on patterns

    Quick Reference Card

    # ========== Creational Patterns ==========
    Singleton    - One instance only
    Factory      - Create objects without specifying class
    Builder      - Construct complex objects step by step
    Prototype    - Clone existing objects
    
    # ========== Structural Patterns ==========
    Adapter      - Convert interface to another
    Decorator    - Add features dynamically
    Facade       - Simplify complex subsystem
    Proxy        - Control access to object
    
    # ========== Behavioral Patterns ==========
    Observer     - Notify multiple objects of changes
    Strategy     - Swap algorithms at runtime
    Command      - Encapsulate request as object
    Template     - Define algorithm skeleton
    

    Next Steps

  • Implement each pattern in your preferred language
  • Identify patterns in existing codebases
  • Refactor code to use appropriate patterns
  • Learn anti-patterns to avoid
  • Study language-specific pattern implementations
  • ---

    Last Updated: 2026-04-20 Difficulty: Intermediate Recommended: Understand OOP before studying patterns

    Topics

    Design PatternsArchitectureOOPBest Practices

    Found This Helpful?

    If you have questions or suggestions for improving these notes, I'd love to hear from you.