Mahdi Shamlou here.
You've mastered Creational Patterns. You understand how to build objects.
You've conquered Structural Patterns. You know how to organize relationships between objects.
Now it's time for the final piece: Behavioral Design Patterns.
This is where things get interesting.
Behavioral patterns answer the hardest question in software design:
How should objects communicate and collaborate?
I've seen systems where:
- Objects were tightly coupled, making changes impossible
- State management was scattered everywhere
- Adding new behaviors required modifying dozens of classes
- Event handling was chaotic and unmaintainable
- Control flow was buried in complex conditionals
Most of these problems disappear when you apply the right behavioral pattern.
The difference between junior and senior developers often comes down to how well they manage behavior and communication between objects.
Let's dive into the patterns that separate them.
What Are Behavioral Design Patterns?
Behavioral patterns focus on how objects interact and distribute responsibility.
While Creational patterns ask: "How do I create objects?"
And Structural patterns ask: "How do I organize objects?"
Behavioral patterns ask: "How do objects talk to each other? Who does what?"
The behavioral patterns are:
- Strategy — Choose an algorithm at runtime
- Observer — Notify multiple objects about state changes
- Command — Encapsulate requests as objects
- State — Change behavior based on internal state
- Template Method — Define algorithm structure, let subclasses fill in details
- Chain of Responsibility — Pass requests through handlers
- Iterator — Traverse collections without exposing implementation details
- Mediator — Centralize communication between objects
- Memento — Save and restore object state
- Visitor — Add operations without changing existing classes
- Interpreter — Define and evaluate a custom language or grammar
Let's explore each with production code.
Behavioral Patterns That Matter
1. Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
It lets the algorithm vary independently from clients that use it.
Imagine a payment system. You support:
- Credit card payments
- PayPal
- Stripe
- Bitcoin
Without Strategy, your checkout code becomes a giant conditional:
if payment_method == "credit_card":
# 50 lines of credit card logic
elif payment_method == "paypal":
# 50 lines of PayPal logic
elif payment_method == "stripe":
# 50 lines of Stripe logic
# ... nightmare
With Strategy, each payment method is a separate, interchangeable algorithm.
When to use it:
- Multiple algorithms for a task
- Different payment methods
- Compression algorithms (zip, gzip, rar)
- Sorting algorithms (quicksort, mergesort, bubblesort)
- Data export formats (JSON, CSV, XML)
- Pricing strategies (standard, premium, enterprise)
- Search/filtering strategies
Bad code (without pattern)
class PaymentProcessor:
def process_payment(self, amount, method):
if method == "credit_card":
# 30 lines of credit card processing
print(f"Processing ${amount} with credit card")
print("Validating card...")
print("Checking fraud detection...")
print("Charging card...")
print("Payment complete")
return True
elif method == "paypal":
# 30 lines of PayPal processing
print(f"Processing ${amount} with PayPal")
print("Redirecting to PayPal...")
print("Waiting for PayPal confirmation...")
print("Payment complete")
return True
elif method == "bitcoin":
# 30 lines of Bitcoin processing
print(f"Processing ${amount} with Bitcoin")
print("Generating wallet address...")
print("Waiting for blockchain confirmation...")
print("Payment complete")
return True
else:
raise ValueError("Unknown payment method")
# Every new payment method requires modifying this class
processor = PaymentProcessor()
processor.process_payment(100, "credit_card")
processor.process_payment(100, "paypal")
Problems:
- Giant conditional logic
- Hard to test each strategy
- Hard to add new strategies
- Violates Single Responsibility Principle
Good code (with Strategy)
from abc import ABC, abstractmethod
# Strategy interface
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount):
pass
# Concrete strategies
class CreditCardStrategy(PaymentStrategy):
def __init__(self, card_number, cvv):
self.card_number = card_number
self.cvv = cvv
def pay(self, amount):
print(f"Processing ${amount} with credit card {self.card_number[-4:]}")
print("Validating card...")
print("Checking fraud detection...")
print("Charging card...")
return True
class PayPalStrategy(PaymentStrategy):
def __init__(self, email):
self.email = email
def pay(self, amount):
print(f"Processing ${amount} with PayPal ({self.email})")
print("Redirecting to PayPal...")
print("Waiting for PayPal confirmation...")
return True
class BitcoinStrategy(PaymentStrategy):
def __init__(self, wallet_address):
self.wallet_address = wallet_address
def pay(self, amount):
print(f"Processing ${amount} with Bitcoin to {self.wallet_address[:10]}...")
print("Generating transaction...")
print("Waiting for blockchain confirmation...")
return True
class StripeStrategy(PaymentStrategy):
def __init__(self, stripe_token):
self.stripe_token = stripe_token
def pay(self, amount):
print(f"Processing ${amount} with Stripe")
print("Contacting Stripe API...")
print("Payment successful")
return True
# Context: Uses the strategy
class PaymentProcessor:
def __init__(self, strategy: PaymentStrategy):
self.strategy = strategy
def process_payment(self, amount):
return self.strategy.pay(amount)
# Client code: Simple and flexible
credit_card = CreditCardStrategy("4111111111111111", "123")
processor = PaymentProcessor(credit_card)
processor.process_payment(100)
paypal = PayPalStrategy("user@example.com")
processor = PaymentProcessor(paypal)
processor.process_payment(100)
bitcoin = BitcoinStrategy("1A1z7agoat")
processor = PaymentProcessor(bitcoin)
processor.process_payment(100)
# Add new strategy? Just create a new class, no modifications needed
Pros:
- Easy to add new algorithms
- Eliminates large conditional statements
- Makes algorithms interchangeable
- Each strategy is testable in isolation
- Follows Single Responsibility Principle
Cons:
- More classes to manage
- Overhead for simple cases
- Clients need to know which strategy to choose
My Take:
Strategy is one of the most practical patterns in real production systems.
Every time you see a big if-elif-elif chain, think Strategy. It's almost always the right answer.
Payment methods, export formats, sorting algorithms, caching strategies—they're all perfect use cases for Strategy.
The key insight: If you're choosing between different ways to do something, Strategy is your pattern.
2. Observer Pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically.
Think about a stock ticker. When a stock price changes, multiple traders, monitors, and alerts need to know immediately.
Without Observer, the stock would need to know about every trader, monitor, and alert (tight coupling).
With Observer, the stock simply notifies anyone who's interested.
When to use it:
- Event systems (UI buttons, input fields)
- Stock ticker systems
- Real-time notifications
- MVC architectures (model changes notify views)
- Pub/Sub systems
- Event-driven architecture
- Social media (following users)
Bad code (without pattern)
class Stock:
def __init__(self, symbol, price):
self.symbol = symbol
self.price = price
self.traders = []
self.monitors = []
self.alerts = []
def set_price(self, new_price):
old_price = self.price
self.price = new_price
# The stock knows about all these systems (bad coupling!)
for trader in self.traders:
trader.notify_price_change(self.symbol, old_price, new_price)
for monitor in self.monitors:
monitor.update_display(self.symbol, new_price)
for alert in self.alerts:
alert.check_threshold(self.symbol, new_price)
def register_trader(self, trader):
self.traders.append(trader)
def register_monitor(self, monitor):
self.monitors.append(monitor)
def register_alert(self, alert):
self.alerts.append(alert)
# Stock knows too much about its dependents
Problems:
- Stock is tightly coupled to all observers
- Adding new observer types requires modifying Stock
- Hard to test
- Violates Single Responsibility
Good code (with Observer)
from abc import ABC, abstractmethod
# Observer interface
class StockObserver(ABC):
@abstractmethod
def update(self, symbol, price):
pass
# Subject
class Stock:
def __init__(self, symbol, price):
self.symbol = symbol
self.price = price
self._observers = []
def attach(self, observer: StockObserver):
"""Subscribe an observer"""
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer: StockObserver):
"""Unsubscribe an observer"""
self._observers.remove(observer)
def notify(self):
"""Notify all observers of changes"""
for observer in self._observers:
observer.update(self.symbol, self.price)
def set_price(self, new_price):
print(f"{self.symbol} price changed from ${self.price} to ${new_price}")
self.price = new_price
self.notify() # Notify all interested parties
# Concrete observers
class Trader(StockObserver):
def __init__(self, name):
self.name = name
def update(self, symbol, price):
print(f"Trader {self.name}: {symbol} is now ${price}")
class Monitor(StockObserver):
def __init__(self, name):
self.name = name
def update(self, symbol, price):
print(f"Monitor {self.name}: Updating display for {symbol} = ${price}")
class PriceAlert(StockObserver):
def __init__(self, name, threshold):
self.name = name
self.threshold = threshold
def update(self, symbol, price):
if price > self.threshold:
print(f"Alert {self.name}: {symbol} exceeded threshold! Price: ${price}")
# Client code
apple = Stock("AAPL", 150)
# Attach observers
trader1 = Trader("Alice")
trader2 = Trader("Bob")
monitor = Monitor("Dashboard")
alert = PriceAlert("HighAlert", 155)
apple.attach(trader1)
apple.attach(trader2)
apple.attach(monitor)
apple.attach(alert)
# Stock doesn't know about specific observer types
# Just notifies them
apple.set_price(152)
apple.set_price(156) # Alert triggers
# Detach if needed
apple.detach(trader1)
apple.set_price(157) # Alice doesn't get notified
Real-World Example: Event System
class Button:
def __init__(self):
self._listeners = []
def on_click(self, listener):
"""Register click listener"""
self._listeners.append(listener)
def click(self):
"""Simulate button click"""
print("Button clicked")
for listener in self._listeners:
listener()
# Usage
button = Button()
button.on_click(lambda: print("Action 1: Save file"))
button.on_click(lambda: print("Action 2: Send notification"))
button.on_click(lambda: print("Action 3: Log event"))
button.click() # All listeners are notified
Pros:
- Loose coupling between subject and observers
- Dynamic subscription/unsubscription
- Multiple observers supported
- Follows Single Responsibility
- Allows broadcast communication
Cons:
- Observer order is unpredictable
- Memory leaks if observers aren't unsubscribed
- Can be hard to debug event chains
- Performance overhead with many observers
My Take:
Observer is fundamental to modern event-driven systems.
You use it constantly:
- React/Vue with event handlers
- Django signals for model changes
- Webhook systems for notifications
- Event buses in microservices
The key insight: When you have one thing changing and many things that need to react, use Observer.
Don't make objects directly call each other. Let them observe and react independently.
3. Command Pattern
The Command pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue requests, and log requests.
It turns an action into a standalone object.
Think about a text editor's undo/redo. Every action (delete text, insert text, format) is a Command object.
To undo, you just pop the last command off the stack and reverse it.
When to use it:
- Undo/redo functionality
- Task queuing and scheduling
- Macro recording
- Asynchronous task execution
- Transaction management
- Request logging
- Callback systems
Bad code (without pattern)
class TextEditor:
def __init__(self):
self.text = ""
self.history = []
self.undo_stack = []
def insert_text(self, text):
self.text += text
self.history.append(("insert", text))
def delete_text(self, count):
self.text = self.text[:-count]
self.history.append(("delete", count))
def make_bold(self, start, end):
# Complex formatting logic
self.history.append(("bold", start, end))
def undo(self):
if not self.history:
return
action = self.history.pop()
if action[0] == "insert":
self.text = self.text[:-len(action[1])]
elif action[0] == "delete":
# Can't easily undo delete without storing deleted text
pass
elif action[0] == "bold":
# How do you undo bold?
pass
def redo(self):
# Redo logic (complicated and fragile)
pass
# Problems:
# - Undo logic is scattered
# - Each action needs special undo handling
# - Hard to add new actions
# - Bug-prone
Good code (with Command)
from abc import ABC, abstractmethod
# Command interface
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
# Concrete commands
class InsertTextCommand(Command):
def __init__(self, editor, text):
self.editor = editor
self.text = text
def execute(self):
self.editor.text += self.text
def undo(self):
self.editor.text = self.editor.text[:-len(self.text)]
class DeleteTextCommand(Command):
def __init__(self, editor, count):
self.editor = editor
self.count = count
self.deleted_text = None
def execute(self):
self.deleted_text = self.editor.text[-self.count:]
self.editor.text = self.editor.text[:-self.count]
def undo(self):
self.editor.text += self.deleted_text
class MakeBoldCommand(Command):
def __init__(self, editor, start, end):
self.editor = editor
self.start = start
self.end = end
self.original_text = None
def execute(self):
self.original_text = self.editor.text[self.start:self.end]
bold_text = f"**{self.original_text}**"
self.editor.text = (
self.editor.text[:self.start] +
bold_text +
self.editor.text[self.end:]
)
def undo(self):
self.editor.text = (
self.editor.text[:self.start] +
self.original_text +
self.editor.text[self.start + len(self.original_text) + 4:]
)
# Invoker
class TextEditor:
def __init__(self):
self.text = ""
self.history = []
self.undo_stack = []
def execute_command(self, command: Command):
command.execute()
self.history.append(command)
def undo(self):
if not self.history:
return
command = self.history.pop()
command.undo()
self.undo_stack.append(command)
def redo(self):
if not self.undo_stack:
return
command = self.undo_stack.pop()
command.execute()
self.history.append(command)
# Client code
editor = TextEditor()
# Execute commands
insert_cmd = InsertTextCommand(editor, "Hello ")
editor.execute_command(insert_cmd)
print(editor.text) # "Hello "
insert_cmd2 = InsertTextCommand(editor, "World")
editor.execute_command(insert_cmd2)
print(editor.text) # "Hello World"
delete_cmd = DeleteTextCommand(editor, 5)
editor.execute_command(delete_cmd)
print(editor.text) # "Hello "
# Undo
editor.undo()
print(editor.text) # "Hello World"
editor.undo()
print(editor.text) # "Hello "
# Redo
editor.redo()
print(editor.text) # "Hello World"
# Each command knows how to execute and undo itself
# Adding new commands is easy
# No special undo logic needed
Pros:
- Decouples sender from receiver
- Undo/redo become trivial
- Easy to queue or schedule commands
- Easy to log command history
- Supports macros and composite commands
Cons:
- More classes for each command
- Memory overhead for storing commands
- Can become complex with many commands
My Take:
Command is essential for any system with undo/redo, queuing, or transaction management.
The brilliance: Each action knows how to do itself and undo itself.
No need for centralized undo logic. Each command is responsible for its own reversal.
This is how professional text editors, design tools, and version control systems work.
4. State Pattern
The State pattern allows an object to alter its behavior when its internal state changes.
The object will appear to change its class.
Think about a traffic light:
Red → Green (can't go to Yellow directly)
Green → Yellow (can't go to Red directly)
Yellow → Red (can't go to Green directly)
Without State, you have giant conditionals:
if self.state == "red":
# Red behavior
elif self.state == "yellow":
# Yellow behavior
elif self.state == "green":
# Green behavior
With State, each state is a separate object that knows what it can do.
When to use it:
- State machines (traffic lights, orders, workflows)
- UI state management
- Game states (menu, playing, paused, game over)
- Order status (pending, processing, shipped, delivered)
- Connection states (connecting, connected, disconnected)
- User states (logged out, logged in, admin)
Bad code (without pattern)
class TrafficLight:
def __init__(self):
self.state = "red"
def next(self):
if self.state == "red":
self.state = "green"
print("🟢 Green light")
elif self.state == "green":
self.state = "yellow"
print("🟡 Yellow light")
elif self.state == "yellow":
self.state = "red"
print("🔴 Red light")
def can_proceed(self):
if self.state == "red":
return False
elif self.state == "yellow":
return False
elif self.state == "green":
return True
def get_description(self):
if self.state == "red":
return "Stop"
elif self.state == "yellow":
return "Prepare to stop"
elif self.state == "green":
return "Go"
# Problems:
# - Conditional logic everywhere
# - Hard to add new states
# - Logic is mixed with state data
# - Transitions aren't clear
Good code (with State)
from abc import ABC, abstractmethod
# State interface
class TrafficLightState(ABC):
@abstractmethod
def next(self, context):
pass
@abstractmethod
def can_proceed(self):
pass
@abstractmethod
def get_description(self):
pass
# Concrete states
class RedLightState(TrafficLightState):
def next(self, context):
context.set_state(GreenLightState())
print("🟢 Green light")
def can_proceed(self):
return False
def get_description(self):
return "Stop"
class GreenLightState(TrafficLightState):
def next(self, context):
context.set_state(YellowLightState())
print("🟡 Yellow light")
def can_proceed(self):
return True
def get_description(self):
return "Go"
class YellowLightState(TrafficLightState):
def next(self, context):
context.set_state(RedLightState())
print("🔴 Red light")
def can_proceed(self):
return False
def get_description(self):
return "Prepare to stop"
# Context
class TrafficLight:
def __init__(self):
self._state = RedLightState()
def set_state(self, state: TrafficLightState):
self._state = state
def next(self):
self._state.next(self)
def can_proceed(self):
return self._state.can_proceed()
def get_description(self):
return self._state.get_description()
# Client code
light = TrafficLight()
print(light.get_description()) # Stop
print(light.can_proceed()) # False
light.next()
print(light.get_description()) # Go
print(light.can_proceed()) # True
light.next()
print(light.get_description()) # Prepare to stop
light.next()
print(light.get_description()) # Stop
# Adding new state? Just create a new class
# No modifications to existing code
Real-World Example: Order Status
class PendingState(TrafficLightState):
def next(self, context):
context.set_state(ProcessingState())
class ProcessingState(TrafficLightState):
def next(self, context):
context.set_state(ShippedState())
class ShippedState(TrafficLightState):
def next(self, context):
context.set_state(DeliveredState())
class DeliveredState(TrafficLightState):
def next(self, context):
pass # Terminal state
# Each state knows valid transitions
# Business logic is clear
Pros:
- Eliminates large conditional statements
- Each state is a separate class
- Easy to add new states
- State transitions are explicit
- Follows Single Responsibility
Cons:
- More classes
- Slight performance overhead
- Can be overkill for simple cases
My Take:
State is perfect for anything with multiple states and transitions.
E-commerce orders, game loops, connection handling, authentication flows—they all benefit from State.
The key insight: If you have state-dependent behavior, make each state a class.
This makes transitions explicit, state logic local, and new states easy to add.
5. Template Method Pattern
The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses.
It lets subclasses redefine certain steps without changing the algorithm's structure.
Think about making different beverages:
1. Boil water
2. Brew the drink material
3. Pour in cup
4. Add condiments
The structure is the same for tea and coffee. Only steps 2 and 4 differ.
When to use it:
- Algorithms with common structure but varying details
- Beverage makers (tea vs coffee)
- Data processing pipelines
- Document generation
- Game loops
- Testing frameworks
- Build processes
Bad code (without pattern)
class CoffeeRecipe:
def prepare_coffee(self):
self.boil_water()
self.brew_coffee()
self.pour_cup()
self.add_sugar()
self.add_cream()
class TeaRecipe:
def prepare_tea(self):
self.boil_water()
self.brew_tea()
self.pour_cup()
self.add_honey()
self.add_lemon()
# Code duplication: boil_water, pour_cup
# Method names are inconsistent
# Client needs to know which method to call
Good code (with Template Method)
from abc import ABC, abstractmethod
class Beverage(ABC):
# Template method: defines algorithm structure
def prepare(self):
self.boil_water()
self.brew()
self.pour_cup()
self.add_condiments()
def boil_water(self):
print("Boiling water...")
def pour_cup(self):
print("Pouring into cup...")
# Hook methods: subclasses override these
@abstractmethod
def brew(self):
pass
@abstractmethod
def add_condiments(self):
pass
class Coffee(Beverage):
def brew(self):
print("Brewing fresh coffee...")
def add_condiments(self):
print("Adding sugar and cream...")
class Tea(Beverage):
def brew(self):
print("Brewing tea...")
def add_condiments(self):
print("Adding honey and lemon...")
class HotChocolate(Beverage):
def brew(self):
print("Mixing cocoa with hot water...")
def add_condiments(self):
print("Adding whipped cream and marshmallows...")
# Client code
def make_beverage(beverage: Beverage):
beverage.prepare() # Same method for all beverages
print("Coffee:")
make_beverage(Coffee())
print("\nTea:")
make_beverage(Tea())
print("\nHot Chocolate:")
make_beverage(HotChocolate())
# Algorithm structure is defined once
# Subclasses only implement what's different
Real-World Example: Data Processing
class DataProcessor(ABC):
def process(self, filename):
data = self.read_file(filename)
data = self.validate(data)
data = self.transform(data)
self.save_results(data)
@abstractmethod
def read_file(self, filename):
pass
@abstractmethod
def validate(self, data):
pass
@abstractmethod
def transform(self, data):
pass
def save_results(self, data):
print("Saving results...")
class CSVProcessor(DataProcessor):
def read_file(self, filename):
print(f"Reading CSV: {filename}")
return ["row1", "row2"]
def validate(self, data):
print("Validating CSV format...")
return data
def transform(self, data):
print("Transforming CSV to objects...")
return data
class JSONProcessor(DataProcessor):
def read_file(self, filename):
print(f"Reading JSON: {filename}")
return [{"key": "value"}]
def validate(self, data):
print("Validating JSON schema...")
return data
def transform(self, data):
print("Transforming JSON to objects...")
return data
# Same process for any format
csv = CSVProcessor()
csv.process("data.csv")
json = JSONProcessor()
json.process("data.json")
Pros:
- Eliminates code duplication
- Defines algorithm structure once
- Makes it easy to add variants
- Follows DRY principle
- Subclasses only implement what's different
Cons:
- Can be rigid if algorithm changes
- Subclasses can't fully control behavior
- Can be harder to understand flow
My Take:
Template Method is excellent for reducing duplication when you have multiple implementations of similar processes.
Any time you see: "The process is mostly the same, but steps 2 and 4 are different," think Template Method.
It's used everywhere:
- Django views inherit from base views
- Testing frameworks have setup/teardown hooks
- Build tools have standard phases
- Game engines have game loops
The key insight: Define the algorithm's skeleton once, let subclasses fill in the details.
6. Chain of Responsibility Pattern
The Chain of Responsibility pattern passes requests along a chain of handlers.
Each handler decides either to process the request or pass it to the next handler.
Think about a support ticket system:
Level 1 Support → Can't resolve?
↓
Level 2 Support → Can't resolve?
↓
Level 3 Support → Can't resolve?
↓
Manager
Each handler tries to solve it. If they can't, they pass it to the next handler.
When to use it:
- Support ticket escalation
- Logging with multiple levels
- Request validation (multiple checks)
- Event handling (parent nodes pass to children)
- Middleware chains
- Approval workflows
- Exception handling
Bad code (without pattern)
class SupportTicket:
def __init__(self, issue, priority):
self.issue = issue
self.priority = priority
self.resolved = False
def handle_ticket(ticket):
# Giant conditional chain
if ticket.priority == 1:
if level1_can_handle(ticket):
print(f"Level 1 resolved: {ticket.issue}")
ticket.resolved = True
else:
# Pass to level 2
if level2_can_handle(ticket):
print(f"Level 2 resolved: {ticket.issue}")
ticket.resolved = True
else:
# Pass to level 3
if level3_can_handle(ticket):
print(f"Level 3 resolved: {ticket.issue}")
ticket.resolved = True
else:
print("Manager will handle")
elif ticket.priority == 2:
# Different logic for priority 2
pass
# Problems:
# - Nested conditionals
# - Hard to add new levels
# - Logic is tightly coupled
Good code (with Chain of Responsibility)
from abc import ABC, abstractmethod
class SupportHandler(ABC):
def __init__(self, next_handler=None):
self.next_handler = next_handler
def handle_ticket(self, ticket):
if self.can_handle(ticket):
self.resolve(ticket)
elif self.next_handler:
self.next_handler.handle_ticket(ticket)
else:
print(f"No one could handle: {ticket.issue}")
@abstractmethod
def can_handle(self, ticket):
pass
@abstractmethod
def resolve(self, ticket):
pass
class Level1Support(SupportHandler):
def can_handle(self, ticket):
return ticket.priority <= 2
def resolve(self, ticket):
print(f"Level 1 resolved: {ticket.issue}")
ticket.resolved = True
class Level2Support(SupportHandler):
def can_handle(self, ticket):
return ticket.priority <= 3
def resolve(self, ticket):
print(f"Level 2 resolved: {ticket.issue}")
ticket.resolved = True
class Level3Support(SupportHandler):
def can_handle(self, ticket):
return ticket.priority <= 4
def resolve(self, ticket):
print(f"Level 3 resolved: {ticket.issue}")
ticket.resolved = True
class Manager(SupportHandler):
def can_handle(self, ticket):
return True # Manager can handle anything
def resolve(self, ticket):
print(f"Manager resolved: {ticket.issue}")
ticket.resolved = True
# Build the chain
level1 = Level1Support()
level2 = Level2Support(level1)
level3 = Level3Support(level2)
manager = Manager(level3)
# Start with level1 (or any handler in the chain)
class SupportTicket:
def __init__(self, issue, priority):
self.issue = issue
self.priority = priority
self.resolved = False
# Handle tickets
ticket1 = SupportTicket("Can't login", priority=1)
level1.handle_ticket(ticket1) # Level 1 handles it
ticket2 = SupportTicket("Database corruption", priority=4)
level1.handle_ticket(ticket2) # Escalates through chain to Manager
ticket3 = SupportTicket("Slow performance", priority=3)
level1.handle_ticket(ticket3) # Level 3 handles it
# Add new handler? Just insert in the chain
# No modifications to existing handlers
Real-World Example: Logging Levels
class Logger(ABC):
def __init__(self, next_logger=None):
self.next_logger = next_logger
def log(self, level, message):
if self.should_log(level):
self.write(message)
if self.next_logger:
self.next_logger.log(level, message)
@abstractmethod
def should_log(self, level):
pass
@abstractmethod
def write(self, message):
pass
class ConsoleLogger(Logger):
def should_log(self, level):
return level >= 1
def write(self, message):
print(f"Console: {message}")
class FileLogger(Logger):
def should_log(self, level):
return level >= 2
def write(self, message):
print(f"File: {message}")
class ErrorLogger(Logger):
def should_log(self, level):
return level >= 3
def write(self, message):
print(f"Error Alert: {message}")
# Build chain
console = ConsoleLogger()
file = FileLogger(console)
error = ErrorLogger(file)
# Log at different levels
error.log(1, "Info message") # Logged by console
error.log(2, "Warning message") # Logged by console and file
error.log(3, "Error message") # Logged by all
Pros:
- Decouples senders from receivers
- Easy to add/remove handlers
- Flexible composition of handlers
- Follows Single Responsibility
Cons:
- No guarantee request is handled
- Harder to debug request flow
- Can impact performance with many handlers
My Take:
Chain of Responsibility is perfect for anything involving escalation or filtering.
You use it constantly:
- Middleware in web frameworks (Django, Flask)
- Event propagation in UI systems
- Logging systems with multiple outputs
- Validation chains checking multiple conditions
- Request processing pipelines
The key insight: Pass the request down the chain until someone handles it.
7. Iterator Pattern
The Iterator pattern provides a way to access elements of a collection sequentially without exposing its underlying representation.
Think about how you loop through lists in Python:
for item in items:
print(item)
You don't care whether items is a list, set, database cursor, or generator.
The iteration mechanism is hidden.
That's Iterator.
When to use it:
- Traversing complex collections
- Tree structures
- Database cursors
- Streaming data
- Paginated APIs
- Graph traversal
Bad code (without pattern)
class BookCollection:
def __init__(self):
self.books = []
def get_book(self, index):
return self.books[index]
library = BookCollection()
for i in range(len(library.books)):
print(library.get_book(i))
Problems:
- Client depends on internal structure
- Traversal logic is duplicated
- Changing storage breaks clients
Good code (with Iterator)
class BookCollection:
def __init__(self):
self._books = []
def add(self, book):
self._books.append(book)
def __iter__(self):
return iter(self._books)
library = BookCollection()
library.add("Clean Code")
library.add("Design Patterns")
library.add("Refactoring")
for book in library:
print(book)
Pros:
- Hides collection implementation
- Simplifies traversal
- Supports multiple traversal strategies
Cons:
- Extra abstraction layer
- Can be unnecessary for simple collections
My Take:
Python gives you Iterator for free with __iter__() and generators.
Every time you use for item in collection, you're using Iterator.
The key insight: Expose data through iteration, not indexes.
8. Mediator Pattern
The Mediator pattern defines an object that encapsulates how other objects interact.
Instead of objects talking directly to each other, they communicate through a central mediator.
Think about an air traffic control tower.
Pilots don't communicate with every other plane.
They talk to the tower.
The tower coordinates everything.
When to use it:
- Chat applications
- UI components
- Workflow orchestration
- Microservice coordination
- Event hubs
- Air traffic systems
Bad code (without pattern)
class User:
def __init__(self, name):
self.name = name
self.contacts = []
def send(self, message):
for contact in self.contacts:
contact.receive(message)
def receive(self, message):
print(message)
Problems:
- Objects become tightly coupled.
- Every participant must know about others.
- Communication logic is duplicated.
- Adding new participants requires updating existing code.
- Interaction flows become difficult to maintain.
Good code (with Mediator)
from abc import ABC, abstractmethod
class ChatMediator(ABC):
@abstractmethod
def send(self, message, sender):
pass
class ChatRoom(ChatMediator):
def __init__(self):
self.users = []
def add_user(self, user):
self.users.append(user)
def send(self, message, sender):
for user in self.users:
if user != sender:
user.receive(message)
class User:
def __init__(self, name, mediator):
self.name = name
self.mediator = mediator
def send(self, message):
self.mediator.send(
f"{self.name}: {message}",
self
)
def receive(self, message):
print(message)
Pros:
- Reduces coupling
- Centralizes communication logic
- Easier to maintain interactions
Cons:
- Mediator can become a god object
My Take:
The key insight: Replace many-to-many communication with one-to-many communication through a coordinator.
9. Memento Pattern
The Memento pattern captures and externalizes an object's internal state so it can be restored later.
Think about:
- Undo/redo
- Game saves
- Checkpoints
- Snapshots
- Database transactions
When to use it:
- Save functionality
- Version history
- Rollbacks
- Checkpoint systems
Bad code (without pattern) — Memento
class Editor:
def __init__(self):
self.text = ""
self.history = []
def write(self, text):
self.text += text
def save(self):
self.history.append(self.text)
def undo(self):
if self.history:
self.text = self.history.pop()
usage = Editor()
usage.write("Hello ")
usage.save()
usage.write("World")
usage.save()
usage.undo()
print(usage.text)
Problems:
- The editor manages both editing and history tracking.
- Internal state is exposed directly.
- Undo logic is tightly coupled to the editor.
- Difficult to support advanced features like redo, snapshots, or branching history.
- Violates the Single Responsibility Principle.
Good code (with Memento)
class EditorMemento:
def __init__(self, text):
self._text = text
def get_state(self):
return self._text
class Editor:
def __init__(self):
self.text = ""
def write(self, text):
self.text += text
def save(self):
return EditorMemento(self.text)
def restore(self, memento):
self.text = memento.get_state()
class History:
def __init__(self):
self._states = []
def push(self, memento):
self._states.append(memento)
def pop(self):
return self._states.pop()
Pros:
- Encapsulates object state
- Simplifies rollback
- Supports history management
Cons:
- Can consume memory
- Expensive for large objects
My Take:
Command + Memento is how professional editors implement undo systems.
The key insight: Save snapshots instead of reverse-engineering state changes.
10. Visitor Pattern
The Visitor pattern lets you add new operations to existing object structures without modifying those objects.
Think about a file system:
- Calculate size
- Export data
- Validate structure
- Generate reports
You don't want to keep modifying file classes every time.
When to use it:
- Stable object structures
- Multiple unrelated operations
- Reporting systems
- Compilers
- AST processing
Bad code (without pattern) — Visitor
class ImageFile:
def __init__(self, size):
self.size = size
def calculate_size(self):
return self.size
def export_json(self):
return {
"type": "image",
"size": self.size
}
def validate(self):
return self.size > 0
class VideoFile:
def __init__(self, size):
self.size = size
def calculate_size(self):
return self.size
def export_json(self):
return {
"type": "video",
"size": self.size
}
def validate(self):
return self.size > 0
Problems:
- Classes grow endlessly.
- Adding new operations requires changing existing code.
- Violates the Open/Closed Principle.
- Business logic becomes scattered across domain objects.
- High risk of introducing bugs when adding features.
Good code (with Visitor)
from abc import ABC, abstractmethod
class Visitor(ABC):
@abstractmethod
def visit_file(self, file):
pass
class FileElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
class ImageFile(FileElement):
def __init__(self, size):
self.size = size
def accept(self, visitor):
visitor.visit_file(self)
class SizeVisitor(Visitor):
def __init__(self):
self.total = 0
def visit_file(self, file):
self.total += file.size
Pros:
- Add new operations easily
- Keeps domain objects clean
- Follows Open/Closed Principle
Cons:
- Hard to add new element types
- More complex syntax
My Take:
Visitor is common in compilers, parsers, and AST processing.
The key insight: When your data structure is stable but operations change frequently, use Visitor.
11. Interpreter Pattern
The Interpreter pattern defines a grammar for a language and provides an interpreter to evaluate sentences in that language.
In simple terms:
It lets you build your own mini-language.
Think about:
- Search query syntax (
status:open AND priority:high) - Mathematical expressions (
5 + 3 * 2) - Rule engines (
age > 18 AND country == "US") - SQL-like filters
- Domain-Specific Languages (DSLs)
Instead of hardcoding endless conditionals, you represent rules as objects that can interpret themselves.
When to use it:
- Expression evaluators
- Rule engines
- Query parsers
- Configuration languages
- DSLs
- Compilers and parsers
Bad code (without pattern)
def evaluate(rule, context):
if rule == "is_admin":
return context["role"] == "admin"
elif rule == "is_premium":
return context["subscription"] == "premium"
elif rule == "is_adult":
return context["age"] >= 18
# This keeps growing forever...
Problems:
- Giant conditional statements
- Hard to extend
- Business rules are scattered
- No composability
Good code (with Interpreter)
from abc import ABC, abstractmethod
class Expression(ABC):
@abstractmethod
def interpret(self, context):
pass
class GreaterThan(Expression):
def __init__(self, field, value):
self.field = field
self.value = value
def interpret(self, context):
return context[self.field] > self.value
class Equals(Expression):
def __init__(self, field, value):
self.field = field
self.value = value
def interpret(self, context):
return context[self.field] == self.value
class And(Expression):
def __init__(self, left, right):
self.left = left
self.right = right
def interpret(self, context):
return (
self.left.interpret(context)
and self.right.interpret(context)
)
# Build expression:
# age > 18 AND subscription == "premium"
rule = And(
GreaterThan("age", 18),
Equals("subscription", "premium")
)
user = {
"age": 25,
"subscription": "premium"
}
print(rule.interpret(user)) # True
Pros:
- Easy to add new expressions
- Business rules become composable
- Grammar is explicit
- Follows Open/Closed Principle
Cons:
- Creates many small classes
- Can become complex for large grammars
- Performance overhead for deep expression trees
My Take:
Interpreter is one of the least-used GoF patterns in everyday application development.
Most developers use existing libraries instead of building interpreters from scratch.
But you'll see it everywhere under the hood:
- SQL parsers
- Regex engines
- Elasticsearch queries
- GraphQL resolvers
- Policy engines
- Template engines
The key insight: If users need to express rules dynamically, create a language instead of adding more conditionals.
Comparison Table
| Pattern | Purpose | Complexity | When to Use |
|---|---|---|---|
| Strategy | Choose algorithm at runtime | Low | Multiple ways to do same thing |
| Observer | Notify multiple objects of state changes | Medium | Event systems, real-time updates |
| Command | Encapsulate requests as objects | Medium | Undo/redo, queuing, logging |
| State | Change behavior based on internal state | Medium | State machines, workflows |
| Template Method | Define algorithm structure in base class | Low | Similar algorithms with different steps |
| Chain of Resp. | Pass requests through handler chain | Medium | Escalation, filtering, middleware |
| Iterator | Traverse collections | Low | Custom collections |
| Mediator | Coordinate object interactions | Medium | Complex communication |
| Memento | Save and restore state | Medium | Snapshots, rollback |
| Visitor | Add operations to objects | High | ASTs, reporting |
| Interpreter | Define and evaluate grammar | High | DSLs, rule engines |
Key Takeaways
- Strategy — Choose behavior dynamically
- Observer — React to state changes automatically
- Command — Encapsulate and queue requests
- State — Manage complex state transitions clearly
- Template Method — Share algorithm structure, differ in details
- Chain of Responsibility — Escalate through handlers
- Iterator — Traverse collections consistently
- Mediator — Centralize communication
- Memento — Save and restore state
- Visitor — Add operations without modifying classes
- Interpreter — Build and evaluate custom rules
The Golden Rule of Behavioral Patterns:
Make object interaction explicit. Don't hide behavior in conditionals.
When you find yourself writing giant if-elif chains or scattered event handling, a behavioral pattern is probably the answer.
Design Patterns Complete Series
- Creational Patterns — How to create objects properly
- Structural Patterns - Part 1 — Adapter, Bridge, Composite, Decorator
- Structural Patterns - Part 2 — Facade, Flyweight, Proxy
- Behavioral Patterns (This Article) — How objects communicate
Want More Deep Dives?
If you enjoyed this article, check out my other production-focused guides:
- Message Brokers in 2026: Kafka, RabbitMQ, NATS — Architecture decisions
- OWASP Top 10 for Developers (2026 Edition) — Security patterns
- Injection Attacks Are Not Dead — Attack patterns and solutions
- Durable Workflow Engines: Temporal vs dbt OS — System patterns
🔗 LinkedIn: https://www.linkedin.com/in/mahdi-shamlou-3b52b8278
📱 Telegram: https://telegram.me/mahdi0shamlou
📸 Instagram: https://www.instagram.com/mahdi0shamlou/
Author: Mahdi Shamlou | مهدی شاملو


Top comments (0)