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
Comments