Skip to main content
โšก Calmops

Design Patterns: A Practical Guide for Software Engineers

Introduction

Design patterns are reusable solutions to commonly occurring problems in software design. They represent best practices evolved over time by experienced software developers. Understanding and applying design patterns helps you write more maintainable, flexible, and scalable code.

In 2026, while new paradigms like functional programming and microservices have emerged, design patterns remain fundamental to software engineering. Whether you’re building object-oriented systems, working with component-based architectures, or designing APIs, these patterns provide proven solutions to recurring design challenges.

This guide covers the most essential design patterns, organized by their purpose: creational patterns for object creation, structural patterns for object composition, and behavioral patterns for object interaction.

Creational Patterns

Singleton Pattern

Ensures a class has only one instance and provides a global point of access to it.

class Singleton:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

# Thread-safe version
import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

Use Cases: Database connections, configuration managers, logging services

Factory Method Pattern

Defines an interface for creating objects, letting subclasses decide which class to instantiate.

from abc import ABC, abstractmethod

class Document(ABC):
    @abstractmethod
    def open(self):
        pass
    
    @abstractmethod
    def save(self):
        pass

class PDFDocument(Document):
    def open(self):
        return "Opening PDF document"
    
    def save(self):
        return "Saving PDF document"

class WordDocument(Document):
    def open(self):
        return "Opening Word document"
    
    def save(self):
        return "Saving Word document"

class DocumentFactory:
    def create_document(self, doc_type):
        if doc_type == 'pdf':
            return PDFDocument()
        elif doc_type == 'word':
            return WordDocument()
        raise ValueError(f"Unknown document type: {doc_type}")

# Usage
factory = DocumentFactory()
doc = factory.create_document('pdf')

Use Cases: Document creation, payment processor selection, database connections

Abstract Factory Pattern

Provides an interface for creating families of related objects without specifying concrete classes.

class Button(ABC):
    @abstractmethod
    def render(self):
        pass

class TextField(ABC):
    @abstractmethod
    def render(self):
        pass

# Windows family
class WindowsButton(Button):
    def render(self):
        return "Rendering Windows button"

class WindowsTextField(TextField):
    def render(self):
        return "Rendering Windows text field"

# Mac family
class MacButton(Button):
    def render(self):
        return "Rendering Mac button"

class MacTextField(TextField):
    def render(self):
        return "Rendering Mac text field"

# Abstract Factory
class UIFactory(ABC):
    @abstractmethod
    def create_button(self):
        pass
    
    @abstractmethod
    def create_text_field(self):
        pass

class WindowsFactory(UIFactory):
    def create_button(self):
        return WindowsButton()
    
    def create_text_field(self):
        return WindowsTextField()

class MacFactory(UIFactory):
    def create_button(self):
        return MacButton()
    
    def create_text_field(self):
        return MacTextField()

# Usage
def create_ui(factory: UIFactory):
    button = factory.create_button()
    text_field = factory.create_text_field()
    return button.render(), text_field.render()

create_ui(WindowsFactory())  # Windows UI
create_ui(MacFactory())      # Mac UI

Use Cases: Cross-platform UI toolkits, theme systems, database adapters

Builder Pattern

Separates the construction of a complex object from its representation.

class Pizza:
    def __init__(self):
        self.size = None
        self.crust = None
        self.toppings = []
    
    def __str__(self):
        return f"Pizza(size={self.size}, crust={self.crust}, toppings={self.toppings})"

class PizzaBuilder:
    def __init__(self):
        self.pizza = Pizza()
    
    def set_size(self, size):
        self.pizza.size = size
        return self
    
    def set_crust(self, crust):
        self.pizza.crust = crust
        return self
    
    def add_topping(self, topping):
        self.pizza.toppings.append(topping)
        return self
    
    def build(self):
        return self.pizza

class PizzaDirector:
    def __init__(self, builder):
        self.builder = builder
    
    def make_margherita(self):
        return (self.builder
                .set_size('medium')
                .set_crust('thin')
                .add_topping('tomato sauce')
                .add_topping('mozzarella')
                .add_topping('basil')
                .build())
    
    def make_meat_lovers(self):
        return (self.builder
                .set_size('large')
                .set_crust('thick')
                .add_topping('tomato sauce')
                .add_topping('mozzarella')
                .add_topping('pepperoni')
                .add_topping('sausage')
                .add_topping('bacon')
                .build())

# Usage
director = PizzaDirector(PizzaBuilder())
margherita = director.make_margherita()
meat_lovers = director.make_meat_lovers()

Use Cases: Complex object construction, SQL query builders, document generators

Prototype Pattern

Creates new objects by cloning an existing object (prototype).

import copy

class Prototype:
    def clone(self):
        return copy.deepcopy(self)

class Document(Prototype):
    def __init__(self, title, content, metadata=None):
        self.title = title
        self.content = content
        self.metadata = metadata or {}
    
    def __str__(self):
        return f"Document(title={self.title}, content={self.content[:30]}...)"

# Usage
original = Document("Original", "This is the original content", {"author": "John"})
cloned = original.clone()
cloned.title = "Cloned"

print(original)  # Document(title=Original, ...)
print(cloned)    # Document(title=Cloned, ...)

Use Cases: Object caching, undo functionality, complex object templates

Structural Patterns

Adapter Pattern

Converts the interface of a class into another interface clients expect.

class OldPaymentSystem:
    def process_payment(self, amount):
        return f"Processing ${amount} via old system"

class NewPaymentSystem:
    def make_payment(self, amount, currency="USD"):
        return f"Processing {currency} {amount} via new system"

class PaymentAdapter:
    def __init__(self, new_system):
        self.new_system = new_system
    
    def process_payment(self, amount):
        # Adapt old interface to new
        return self.new_system.make_payment(amount)

# Usage
adapter = PaymentAdapter(NewPaymentSystem())
result = adapter.process_payment(100)  # "Processing USD 100 via new system"

Use Cases: Legacy system integration, third-party library integration, API compatibility

Decorator Pattern

Attaches additional responsibilities to an object dynamically.

class Coffee:
    def cost(self):
        return 5
    
    def description(self):
        return "Coffee"

class CoffeeDecorator:
    def __init__(self, coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost()
    
    def description(self):
        return self.coffee.description()

class Milk(CoffeeDecorator):
    def cost(self):
        return self.coffee.cost() + 1.5
    
    def description(self):
        return self.coffee.description() + ", Milk"

class Sugar(CoffeeDecorator):
    def cost(self):
        return self.coffee.cost() + 0.5
    
    def description(self):
        return self.coffee.description() + ", Sugar"

class WhippedCream(CoffeeDecorator):
    def cost(self):
        return self.coffee.cost() + 2
    
    def description(self):
        return self.coffee.description() + ", Whipped Cream"

# Usage
coffee = Coffee()
coffee_with_milk = Milk(coffee)
coffee_with_milk_and_sugar = Sugar(coffee_with_milk)
fancy_coffee = WhippedCream(Sugar(Milk(Coffee())))

print(fancy_coffee.description())  # "Coffee, Milk, Sugar, Whipped Cream"
print(fancy_coffee.cost())         # 9.0

Use Cases: Adding features to objects, stream wrappers, middleware

Facade Pattern

Provides a simplified interface to a complex subsystem.

class VideoFile:
    def __init__(self, filename):
        self.filename = filename
        self.codec = self._extract_codec(filename)
    
    def _extract_codec(self, filename):
        return "mpeg" if filename.endswith('.mp4') else 'unknown'

class CodecFactory:
    def extract(self, video_file):
        return video_file.codec

class AudioMixer:
    def fix(self, result):
        return "Audio fixed"

class VideoConverter:
    def convert(self, filename, format):
        file = VideoFile(filename)
        codec = CodecFactory().extract(file)
        
        # Complex conversion logic hidden behind facade
        audio = AudioMixer().fix(codec)
        
        return f"Converted {filename} to {format}"

# Usage - simple interface
converter = VideoConverter()
result = converter.convert("movie.ogg", "mp4")

Use Cases: Library wrappers, complex system simplification, API gateways

Composite Pattern

Composes objects into tree structures to represent part-whole hierarchies.

class Component(ABC):
    @abstractmethod
    def operation(self):
        pass

class Leaf(Component):
    def __init__(self, name):
        self.name = name
    
    def operation(self):
        return f"Leaf {self.name}"

class Composite(Component):
    def __init__(self, name):
        self.name = name
        self.children = []
    
    def add(self, component):
        self.children.append(component)
    
    def remove(self, component):
        self.children.remove(component)
    
    def operation(self):
        results = [child.operation() for child in self.children]
        return f"Composite {self.name}: [{', '.join(results)}]"

# Usage
tree = Composite("root")
branch1 = Composite("branch1")
branch1.add(Leaf("leaf1"))
branch1.add(Leaf("leaf2"))
branch2 = Composite("branch2")
branch2.add(Leaf("leaf3"))
tree.add(branch1)
tree.add(branch2)

print(tree.operation())
# "Composite root: [Composite branch1: [Leaf leaf1, Leaf leaf2], Composite branch2: [Leaf leaf3]]"

Use Cases: File systems, UI hierarchies, organization structures

Proxy Pattern

Provides a surrogate or placeholder for another object to control access to it.

class RealImage:
    def __init__(self, filename):
        self.filename = filename
        self._load_from_disk()
    
    def _load_from_disk(self):
        print(f"Loading {self.filename} from disk...")
    
    def display(self):
        return f"Displaying {self.filename}"

class ProxyImage:
    def __init__(self, filename):
        self.filename = filename
        self._real_image = None
    
    def display(self):
        if self._real_image is None:
            self._real_image = RealImage(self.filename)
        return self._real_image.display()

# Usage
# Image is not loaded until display() is called
proxy = ProxyImage("photo.jpg")
print("Proxy created")

# Now image is loaded
print(proxy.display())

# Second call uses cached image
print(proxy.display())

Use Cases: Lazy loading, access control, logging, caching

Behavioral Patterns

Observer Pattern

Defines a one-to-many dependency between objects so when one object changes state, all its dependents are notified.

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

class Subject:
    def __init__(self):
        self._observers = []
    
    def attach(self, observer):
        self._observers.append(observer)
    
    def detach(self, observer):
        self._observers.remove(observer)
    
    def notify(self, message):
        for observer in self._observers:
            observer.update(message)

class NewsAgency(Subject):
    def __init__(self):
        super().__init__()
        self._latest_news = None
    
    def set_news(self, news):
        self._latest_news = news
        self.notify(news)

class NewsChannel(Observer):
    def __init__(self, name):
        self.name = name
    
    def update(self, message):
        print(f"{self.name} received: {message}")

# Usage
agency = NewsAgency()
cnn = NewsChannel("CNN")
bbc = NewsChannel("BBC")

agency.attach(cnn)
agency.attach(bbc)

agency.set_news("Breaking news: AI advances rapidly!")
# CNN received: Breaking news: AI advances rapidly!
# BBC received: Breaking news: AI advances rapidly!

Use Cases: Event handling, MVC, notifications, message queues

Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data):
        pass

class BubbleSort(SortStrategy):
    def sort(self, data):
        # Simple but inefficient
        arr = data.copy()
        n = len(arr)
        for i in range(n):
            for j in range(0, n-i-1):
                if arr[j] > arr[j+1]:
                    arr[j], arr[j+1] = arr[j+1], arr[j]
        return arr

class QuickSort(SortStrategy):
    def sort(self, data):
        arr = data.copy()
        self._quicksort(arr, 0, len(arr) - 1)
        return arr
    
    def _quicksort(self, arr, low, high):
        if low < high:
            pi = self._partition(arr, low, high)
            self._quicksort(arr, low, pi - 1)
            self._quicksort(arr, pi + 1, high)
    
    def _partition(self, arr, low, high):
        pivot = arr[high]
        i = low - 1
        for j in range(low, high):
            if arr[j] <= pivot:
                i += 1
                arr[i], arr[j] = arr[j], arr[i]
        arr[i+1], arr[high] = arr[high], arr[i+1]
        return i + 1

class Sorter:
    def __init__(self, strategy):
        self.strategy = strategy
    
    def set_strategy(self, strategy):
        self.strategy = strategy
    
    def sort(self, data):
        return self.strategy.sort(data)

# Usage
sorter = Sorter(QuickSort())
result = sorter.sort([64, 34, 25, 12, 22, 11, 90])

sorter.set_strategy(BubbleSort())
result = sorter.sort([64, 34, 25, 12, 22, 11, 90])

Use Cases: Sorting algorithms, payment processing, compression algorithms

Command Pattern

Encapsulates a request as an object, allowing parameterization and queuing of requests.

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

class Light:
    def on(self):
        return "Light is ON"
    
    def off(self):
        return "Light is OFF"

class LightOnCommand(Command):
    def __init__(self, light):
        self.light = light
    
    def execute(self):
        return self.light.on()

class LightOffCommand(Command):
    def __init__(self, light):
        self.light = light
    
    def execute(self):
        return self.light.off()

class RemoteControl:
    def __init__(self):
        self._commands = {}
    
    def set_command(self, slot, command):
        self._commands[slot] = command
    
    def press_button(self, slot):
        if slot in self._commands:
            return self._commands[slot].execute()
        return "No command set"

# Usage
light = Light()
light_on = LightOnCommand(light)
light_off = LightOffCommand(light)

remote = RemoteControl()
remote.set_command(0, light_on)
remote.set_command(1, light_off)

remote.press_button(0)  # "Light is ON"
remote.press_button(1)  # "Light is OFF"

Use Cases: Undo/redo, macro recording, task scheduling, transaction management

State Pattern

Allows an object to alter its behavior when its internal state changes.

class State(ABC):
    @abstractmethod
    def insert_coin(self, machine):
        pass
    
    @abstractmethod
    def eject_coin(self, machine):
        pass
    
    @abstractmethod
    def turn_crank(self, machine):
        pass

class NoCoinState(State):
    def insert_coin(self, machine):
        print("Coin inserted")
        machine.state = machine.has_coin_state
    
    def eject_coin(self, machine):
        print("No coin to return")
    
    def turn_crank(self, machine):
        print("Insert coin first")

class HasCoinState(State):
    def insert_coin(self, machine):
        print("Coin already inserted")
    
    def eject_coin(self, machine):
        print("Coin returned")
        machine.state = machine.no_coin_state
    
    def turn_crank(self, machine):
        print("Crank turned")
        machine.state = machine.sold_state

class SoldState(State):
    def insert_coin(self, machine):
        print("Please wait")
    
    def eject_coin(self, machine):
        print("Already turned crank")
    
    def turn_crank(self, machine):
        print("Already turned crank")

class VendingMachine:
    def __init__(self):
        self.no_coin_state = NoCoinState()
        self.has_coin_state = HasCoinState()
        self.sold_state = SoldState()
        self.state = self.no_coin_state
    
    def insert_coin(self):
        self.state.insert_coin(self)
    
    def eject_coin(self):
        self.state.eject_coin(self)
    
    def turn_crank(self):
        self.state.turn_crank(self)

# Usage
machine = VendingMachine()
machine.insert_coin()   # "Coin inserted"
machine.turn_crank()    # "Crank turned"
machine.insert_coin()   # "Please wait"

Use Cases: State machines, workflow engines, game development

Template Method Pattern

Defines the skeleton of an algorithm, deferring some steps to subclasses.

class DataMiner(ABC):
    # Template method
    def mine(self, path):
        file = self.open_file(path)
        raw_data = self.extract_data(file)
        data = self.parse_data(raw_data)
        analysis = self.analyze_data(data)
        self.send_report(analysis)
        return analysis
    
    @abstractmethod
    def open_file(self, path):
        pass
    
    @abstractmethod
    def extract_data(self, file):
        pass
    
    def parse_data(self, raw_data):
        # Common implementation
        return raw_data.split(',')
    
    @abstractmethod
    def analyze_data(self, data):
        pass
    
    def send_report(self, analysis):
        print(f"Report: {analysis}")

class PDFDataMiner(DataMiner):
    def open_file(self, path):
        return f"PDF file: {path}"
    
    def extract_data(self, file):
        return "PDF raw data"
    
    def analyze_data(self, data):
        return f"PDF analysis: {data}"

class CSVDataMiner(DataMiner):
    def open_file(self, path):
        return f"CSV file: {path}"
    
    def extract_data(self, file):
        return "CSV raw data"
    
    def analyze_data(self, data):
        return f"CSV analysis: {data}"

# Usage
pdf_miner = PDFDataMiner()
result = pdf_miner.mine("report.pdf")

csv_miner = CSVDataMiner()
result = csv_miner.mine("data.csv")

Use Cases: Frameworks, data processing pipelines, document generators

Iterator Pattern

Provides a way to access elements of a collection sequentially without exposing its underlying representation.

class Iterator(ABC):
    @abstractmethod
    def has_next(self):
        pass
    
    @abstractmethod
    def next(self):
        pass

class ListIterator(Iterator):
    def __init__(self, collection):
        self.collection = collection
        self.position = 0
    
    def has_next(self):
        return self.position < len(self.collection)
    
    def next(self):
        if self.has_next():
            item = self.collection[self.position]
            self.position += 1
            return item
        raise StopIteration()

class ReverseIterator(Iterator):
    def __init__(self, collection):
        self.collection = collection
        self.position = len(collection) - 1
    
    def has_next(self):
        return self.position >= 0
    
    def next(self):
        if self.has_next():
            item = self.collection[self.position]
            self.position -= 1
            return item
        raise StopIteration()

# Usage
collection = [1, 2, 3, 4, 5]

iterator = ListIterator(collection)
while iterator.has_next():
    print(iterator.next())  # 1, 2, 3, 4, 5

reverse_iterator = ReverseIterator(collection)
while reverse_iterator.has_next():
    print(reverse_iterator.next())  # 5, 4, 3, 2, 1

Use Cases: Collections traversal, database result sets, tree/graph traversal

Conclusion

Design patterns provide proven solutions to common software design problems. While it’s important to understand these patterns, equally important is knowing when NOT to use them. Over-engineering with patterns where they’re not needed can add unnecessary complexity.

Key takeaways:

  • Creational patterns focus on object creation mechanisms
  • Structural patterns deal with object composition
  • Behavioral patterns define object communication patterns
  • Apply patterns when you recognize the problem they solve
  • Don’t force patterns where simple solutions work better

Resources

Comments