Loading episodes…
0:00 0:00

Visually Explained: The Chain of Responsibility Pattern in Python

00:00
BACK TO HOME

Visually Explained: The Chain of Responsibility Pattern in Python

10xTeam November 21, 2025 10 min read

The Chain of Responsibility is a behavioral design pattern that lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.

Imagine a multi-level customer support system. When you call, you first talk to a Level 1 operator. If they can’t solve your issue, they don’t just hang up; they transfer you to a Level 2 specialist. If the specialist is also stumped, they escalate it to an engineer. Your request travels along a chain of potential problem-solvers until one of them handles it. That’s the Chain of Responsibility in a nutshell.

This pattern decouples the sender of a request from its receivers, giving more than one object a chance to handle the request.

The Problem: The God Object

Let’s consider a common scenario: processing a new e-commerce order. A single method might be responsible for everything:

  1. Authenticating the user.
  2. Authorizing if the user can make purchases.
  3. Checking if the product is in stock.
  4. Verifying the user has enough balance.
  5. Processing the payment.
  6. Creating the order in the database.
  7. Sending a notification email.

This leads to a massive, tightly-coupled function that is difficult to read, maintain, and test.

class OrderProcessor:
    def process(self, user, product, quantity):
        # 1. Authentication
        if not user.is_authenticated:
            raise Exception("User is not authenticated.")
        print("✅ User Authenticated")

        # 2. Authorization
        if not user.can_purchase:
            raise Exception("User is not authorized to purchase.")
        print("✅ User Authorized")

        # 3. Stock Check
        if product.stock < quantity:
            raise Exception("Product is out of stock.")
        print("✅ Product in Stock")

        # 4. Balance Check
        total_price = product.price * quantity
        if user.balance < total_price:
            raise Exception("Insufficient balance.")
        print("✅ Balance Sufficient")

        # 5. Payment Processing
        print(f"Processing payment of ${total_price}...")
        user.balance -= total_price
        print("✅ Payment Successful")

        # 6. Order Creation
        print("Creating order...")
        new_order = {"product_id": product.id, "quantity": quantity, "total": total_price}
        print(f"✅ Order {new_order} created.")

        # 7. Notification
        print(f"Sending confirmation email to {user.email}...")
        print("✅ Process Complete")

        return new_order

This OrderProcessor violates two key SOLID principles:

  • Single Responsibility Principle (SRP): The class does far more than one thing. It’s an authenticator, authorizer, inventory manager, and payment processor all in one.
  • Open/Closed Principle (OCP): To add a new step (e.g., applying a discount), you must modify this already complex method, risking the introduction of new bugs.

Visually, the logic is a tangled mess inside one object:

graph TD;
    subgraph OrderProcessor
        A[Authenticate] --> B[Authorize];
        B --> C[Check Stock];
        C --> D[Check Balance];
        D --> E[Process Payment];
        E --> F[Create Order];
        F --> G[Send Notification];
    end
    Request --> A;
    G --> Response;

The Solution: Building a Chain of Handlers

The Chain of Responsibility pattern refactors this by breaking down each step into its own Handler class. These handlers are then linked together in a chain.

The flow becomes clean and linear:

graph TD;
    Request --> AuthHandler;
    AuthHandler -- "Pass" --> AuthzHandler;
    AuthzHandler -- "Pass" --> StockHandler;
    StockHandler -- "Pass" --> BalanceHandler;
    BalanceHandler -- "Pass" --> PaymentHandler;
    PaymentHandler -- "Pass" --> OrderCreationHandler;
    OrderCreationHandler -- "Pass" --> NotificationHandler;
    NotificationHandler --> Response;

Let’s build this step-by-step.

Step 1: Define the Project Structure

A clean structure helps organize our handlers.

order_processing/
├── handlers/
│   ├── __init__.py
│   ├── abstract_handler.py
│   ├── auth_handler.py
│   ├── authz_handler.py
│   ├── stock_handler.py
│   └── ... (other handlers)
├── models.py
└── main.py

Step 2: Create the Handler Interface

We’ll start with an abstract base class (ABC) that defines the contract for all handlers. Each handler must have a set_next method to link to the next handler and a handle method to process the request.

handlers/abstract_handler.py:

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Optional

class AbstractHandler(ABC):
    _next_handler: Optional[AbstractHandler] = None

    def set_next(self, handler: AbstractHandler) -> AbstractHandler:
        self._next_handler = handler
        # Returning the handler allows for convenient chaining like:
        # handler1.set_next(handler2).set_next(handler3)
        return handler

    @abstractmethod
    def handle(self, request: Any) -> Optional[str]:
        if self._next_handler:
            return self._next_handler.handle(request)
        return None

[!TIP] The handle method in the base class contains the core logic for passing the request down the chain. A concrete handler will call super().handle(request) to delegate to the next handler.

Step 3: Implement Concrete Handlers

Now, we extract the logic from our monolithic OrderProcessor into individual handler classes. Each class is small, focused, and respects the Single Responsibility Principle.

Let’s look at the AuthenticationHandler.

handlers/auth_handler.py:

from typing import Any, Optional
from .abstract_handler import AbstractHandler

class AuthenticationHandler(AbstractHandler):
    def handle(self, request: Any) -> Optional[str]:
        if not request['user'].is_authenticated:
            return "Error: User is not authenticated."
        
        print("✅ User Authenticated")
        # Pass the request to the next handler in the chain
        return super().handle(request)

And the StockHandler:

handlers/stock_handler.py:

from typing import Any, Optional
from .abstract_handler import AbstractHandler

class StockHandler(AbstractHandler):
    def handle(self, request: Any) -> Optional[str]:
        product = request['product']
        quantity = request['quantity']
        
        if product.stock < quantity:
            return f"Error: Not enough {product.name} in stock."
            
        print("✅ Product in Stock")
        return super().handle(request)

Step 4: The Client Assembles the Chain

The client is responsible for building the chain of handlers in the desired order.

main.py:

# Assume other handlers (Authz, Balance, Payment, etc.) are defined
from handlers.auth_handler import AuthenticationHandler
from handlers.authz_handler import AuthorizationHandler
from handlers.stock_handler import StockHandler
# ... import other handlers

# 1. Create handler instances
auth_handler = AuthenticationHandler()
authz_handler = AuthorizationHandler()
stock_handler = StockHandler()
# ... create other handlers

# 2. Build the chain
auth_handler.set_next(authz_handler).set_next(stock_handler)
# ... chain the rest

# 3. The client sends a request to the first handler
request_data = {'user': current_user, 'product': selected_product, 'quantity': 2}
result = auth_handler.handle(request_data)

if result:
    print(f"Processing failed: {result}")
else:
    print("🎉 Order processed successfully!")

By using this pattern, we’ve transformed our rigid OrderProcessor into a flexible and extensible system. Adding a new step, like a DiscountHandler, is now trivial: just create the new handler and insert it into the chain without touching any existing handler code.

Visualizing the Class Structure

Here is how the classes relate to each other.

classDiagram
    direction LR
    class AbstractHandler {
        <<abstract>>
        +set_next(handler) AbstractHandler
        +handle(request) Optional~str~
    }
    class AuthenticationHandler {
        +handle(request) Optional~str~
    }
    class AuthorizationHandler {
        +handle(request) Optional~str~
    }
    class StockHandler {
        +handle(request) Optional~str~
    }
    class Client {
        +build_chain()
        +send_request()
    }

    AbstractHandler <|-- AuthenticationHandler
    AbstractHandler <|-- AuthorizationHandler
    AbstractHandler <|-- StockHandler
    Client ..> AbstractHandler : uses

Advanced Concepts & Best Practices

Full Execution vs. Short-Circuiting

The example above is a “full execution” chain, where every handler gets a chance to process the request (as long as no errors occur).

Another common type is a “short-circuiting” chain. In this variant, the chain stops as soon as one handler fully processes the request. This is useful when a request can be handled by different specialists, and only one needs to act.

Here’s how a short-circuiting handler might look:

 class CachingHandler(AbstractHandler):
     def handle(self, request: Any) -> Optional[str]:
         if cache.has(request['key']):
             print("✅ Found in cache.")
-            # This was wrong, we should return the value, not continue
-            return super().handle(request)
+            # Return the cached value and STOP the chain.
+            return cache.get(request['key'])
+        
+        # Not in cache, pass to the next handler (e.g., a database fetcher)
+        return super().handle(request)

Handling Unprocessed Requests

[!WARNING] What if a request goes through the entire chain and no handler processes it? In our implementation, handle would return None. The client code must be prepared for this outcome and handle it gracefully, perhaps by throwing an exception or returning a default response.

When to Use the Chain of Responsibility Pattern

  • When you want to decouple a request’s sender and receivers.
  • When more than one object can handle a request, and the handler is determined at runtime.
  • When you have a sequence of processing steps that must be executed, and you want the flexibility to add, remove, or reorder these steps. A great example is middleware in web frameworks (like Express.js or Django).

Conclusion

The Chain of Responsibility pattern is a powerful tool for building clean, decoupled, and maintainable software. By transforming a monolithic process into a flexible pipeline of independent handlers, you adhere to core SOLID principles and create a system that is far easier to evolve. You replace a rigid, complex block of code with a flexible, composable, and understandable chain of operations.

Quiz: Test Your Knowledge
**Question 1:** If you need to add a new `ApplyDiscountHandler` to the order processing chain, where should you add it? **Answer:** It depends on the business logic. A good place would be after `StockHandler` but before `BalanceHandler`, so the discount is applied before the final price is checked against the user's balance. This demonstrates the flexibility of the pattern—you can insert it anywhere you need!
**Question 2:** What is the primary SOLID principle that the Chain of Responsibility pattern helps enforce? **Answer:** The **Single Responsibility Principle (SRP)** and the **Open/Closed Principle (OCP)**. Each handler has a single responsibility, and the system is open to extension (by adding new handlers) but closed for modification (existing handlers don't need to be changed).

Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?