DEV Community

Mahdi Shamlou | Behavioral Design Patterns 2026: Strategy, Observer, Command, State & More — Production Examples

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.

Mahdi Shamlou

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:

  1. Strategy — Choose an algorithm at runtime
  2. Observer — Notify multiple objects about state changes
  3. Command — Encapsulate requests as objects
  4. State — Change behavior based on internal state
  5. Template Method — Define algorithm structure, let subclasses fill in details
  6. Chain of Responsibility — Pass requests through handlers
  7. Iterator — Traverse collections without exposing implementation details
  8. Mediator — Centralize communication between objects
  9. Memento — Save and restore object state
  10. Visitor — Add operations without changing existing classes
  11. 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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Without State, you have giant conditionals:

if self.state == "red":
    # Red behavior
elif self.state == "yellow":
    # Yellow behavior
elif self.state == "green":
    # Green behavior
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Strategy — Choose behavior dynamically
  2. Observer — React to state changes automatically
  3. Command — Encapsulate and queue requests
  4. State — Manage complex state transitions clearly
  5. Template Method — Share algorithm structure, differ in details
  6. Chain of Responsibility — Escalate through handlers
  7. Iterator — Traverse collections consistently
  8. Mediator — Centralize communication
  9. Memento — Save and restore state
  10. Visitor — Add operations without modifying classes
  11. 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

  1. Creational Patterns — How to create objects properly
  2. Structural Patterns - Part 1 — Adapter, Bridge, Composite, Decorator
  3. Structural Patterns - Part 2 — Facade, Flyweight, Proxy
  4. Behavioral Patterns (This Article) — How objects communicate

Want More Deep Dives?

Mahdi Shamlou

If you enjoyed this article, check out my other production-focused guides:


🔗 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)