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:
- Authenticating the user.
- Authorizing if the user can make purchases.
- Checking if the product is in stock.
- Verifying the user has enough balance.
- Processing the payment.
- Creating the order in the database.
- 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
handlemethod in the base class contains the core logic for passing the request down the chain. A concrete handler will callsuper().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,
handlewould returnNone. 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).