Table of Contents
- What is the Chain of Responsibility Pattern?
- When to Use Chain of Responsibility
- Step-by-Step Implementation in Python
- Advanced Use Cases and Variations
- Real-World Examples
- Best Practices
- Conclusion
- References
What is the Chain of Responsibility Pattern?
Definition
The Chain of Responsibility is a behavioral design pattern that enables an object to pass a request along a chain of potential handlers. Each handler in the chain decides whether to process the request or pass it to the next handler. The request travels through the chain until a handler processes it or the chain ends.
This pattern promotes loose coupling between the sender of a request and its receivers: the sender doesn’t need to know which handler will process the request, and handlers can be added or removed dynamically.
Key Components
The pattern consists of four core components:
-
Handler (Interface/Abstract Class)
Defines the interface for handling requests. It declares a method for processing requests (handle()) and a method for setting the next handler in the chain (set_next()). -
Concrete Handlers
Implement thehandle()method. Each concrete handler either processes the request (if it can) or passes it to the next handler in the chain. -
Client
Creates the chain of handlers and sends the initial request to the first handler in the chain. The client is unaware of which handler will process the request. -
Request
The data or object being processed (e.g., a support ticket, log message, or HTTP request).
When to Use Chain of Responsibility
Use Cases
Apply the Chain of Responsibility pattern when:
- Multiple objects can handle a request, and the handler isn’t known at compile time.
- You want to issue a request to one of several objects without explicitly specifying the receiver.
- The set of handlers for a request needs to be dynamic (e.g., adding/removing handlers at runtime).
Benefits
- Decouples Sender and Receiver: The sender doesn’t need to know which handler processes the request, reducing dependencies.
- Simplifies Object Design: Each handler focuses on a single responsibility (e.g., validating email, handling Level 2 support), making code easier to test and maintain.
- Dynamic Flexibility: Handlers can be added, removed, or reordered without changing the client or other handlers.
When Not to Use
Avoid the pattern if:
- The request must be processed by exactly one handler, and there’s a risk the chain will leave it unprocessed.
- The chain becomes too long, leading to performance issues (each request traverses multiple handlers unnecessarily).
- A simpler solution (e.g., a
if-elif-elsechain) suffices for a small, fixed set of handlers.
Step-by-Step Implementation in Python
Let’s implement the Chain of Responsibility pattern with a practical example: a support ticket system. Tickets are routed to Level 1, Level 2, or Specialist support, depending on issue complexity.
Example Scenario: Support Ticket System
- Level 1 Support: Handles simple issues (e.g., “password reset”, “account lock”).
- Level 2 Support: Handles moderate issues (e.g., “software installation”, “network connectivity”).
- Specialist Support: Handles complex issues (e.g., “database corruption”, “API outage”).
Defining the Handler Interface
First, we define an abstract base class (ABC) for the handler. This ensures all concrete handlers implement the required handle() method and support chaining via set_next().
from abc import ABC, abstractmethod
class SupportHandler(ABC):
"""Abstract base class for support handlers."""
def __init__(self):
self.next_handler = None # Reference to the next handler in the chain
def set_next(self, handler):
"""Set the next handler in the chain. Returns the handler for chaining."""
self.next_handler = handler
return handler # Enable chaining: handler1.set_next(handler2).set_next(handler3)
@abstractmethod
def handle(self, issue: str) -> str:
"""Process the issue or pass it to the next handler."""
pass
set_next(): Links handlers together. Returninghandlerallows method chaining (e.g.,level1.set_next(level2).set_next(specialist)).handle(): Abstract method to process the request. Concrete handlers will implement this.
Creating Concrete Handlers
Next, we implement concrete handlers for each support level.
Level 1 Support Handler
Handles simple issues and passes unhandled issues to Level 2.
class Level1Support(SupportHandler):
def handle(self, issue: str) -> str:
simple_issues = {"password reset", "account lock", "profile update"}
if issue.lower() in simple_issues:
return f"✅ Level 1 Support resolved: '{issue}'"
elif self.next_handler:
return self.next_handler.handle(issue) # Pass to next handler
else:
return f"❌ No handler available for: '{issue}'"
Level 2 Support Handler
Handles moderate issues and passes unhandled issues to Specialists.
class Level2Support(SupportHandler):
def handle(self, issue: str) -> str:
moderate_issues = {"software installation", "network connectivity", "printer setup"}
if issue.lower() in moderate_issues:
return f"✅ Level 2 Support resolved: '{issue}'"
elif self.next_handler:
return self.next_handler.handle(issue)
else:
return f"❌ No handler available for: '{issue}'"
Specialist Support Handler
Handles all remaining issues (no next handler).
class SpecialistSupport(SupportHandler):
def handle(self, issue: str) -> str:
# Specialist handles all unprocessed issues
return f"✅ Specialist Support resolved: '{issue}'"
Building the Chain
The client creates handlers and links them into a chain using set_next().
# Create handlers
level1 = Level1Support()
level2 = Level2Support()
specialist = SpecialistSupport()
# Build the chain: Level1 → Level2 → Specialist
level1.set_next(level2).set_next(specialist) # Chaining via set_next() return value
Client Code
The client sends requests to the first handler in the chain. The request travels through the chain until a handler processes it.
# Test different issues
issues = [
"password reset",
"network connectivity",
"database corruption",
"invalid issue" # Unhandled (but Specialist will catch it)
]
for issue in issues:
print(level1.handle(issue))
Output
✅ Level 1 Support resolved: 'password reset'
✅ Level 2 Support resolved: 'network connectivity'
✅ Specialist Support resolved: 'database corruption'
✅ Specialist Support resolved: 'invalid issue'
Explanation:
- “password reset” is handled by Level 1.
- “network connectivity” is passed to Level 2.
- “database corruption” and “invalid issue” reach the Specialist, who handles them.
Advanced Use Cases and Variations
Dynamic Chain Construction
Handlers can be added/removed dynamically (e.g., based on config or user input). For example, load handlers from a JSON config:
import json
from typing import List
def build_chain_from_config(config_path: str) -> SupportHandler:
"""Build a handler chain from a JSON config file."""
with open(config_path) as f:
config = json.load(f) # e.g., ["Level1Support", "Level2Support", "SpecialistSupport"]
handlers: List[SupportHandler] = []
for handler_name in config:
# Dynamically import and instantiate handlers (simplified example)
handler_class = globals()[handler_name]
handlers.append(handler_class())
# Link handlers
for i in range(len(handlers) - 1):
handlers[i].set_next(handlers[i + 1])
return handlers[0] if handlers else None
# Usage:
# chain = build_chain_from_config("support_chain_config.json")
Handling Requests with State
Requests can include metadata (e.g., priority, user ID). Handlers can use this state to decide processing.
class Ticket:
def __init__(self, issue: str, priority: int):
self.issue = issue
self.priority = priority # 1 (low) → 5 (critical)
class PriorityLevel1Support(SupportHandler):
def handle(self, ticket: Ticket) -> str:
if ticket.priority <= 2 and ticket.issue in ["password reset"]:
return f"✅ Level 1 (Priority {ticket.priority}): {ticket.issue}"
elif self.next_handler:
return self.next_handler.handle(ticket)
return "❌ Unhandled"
Combining with Other Patterns
- Decorator Pattern: Add cross-cutting concerns (e.g., logging, timing) to handlers without modifying their core logic.
- Command Pattern: Encapsulate requests as
Commandobjects, allowing handlers to process different request types uniformly.
Real-World Examples
Logging Systems
Logging frameworks (e.g., Python’s logging module) use a chain of handlers (console, file, email) to route log messages based on severity (DEBUG, INFO, WARNING, ERROR).
class ConsoleLogger(SupportHandler):
def handle(self, log: dict) -> str:
if log["level"] >= 10: # DEBUG=10, INFO=20, etc.
print(f"[CONSOLE] {log['message']}")
if self.next_handler:
return self.next_handler.handle(log)
return "Logged"
class FileLogger(SupportHandler):
def handle(self, log: dict) -> str:
if log["level"] >= 20: # Log INFO+ to file
with open("app.log", "a") as f:
f.write(f"[FILE] {log['message']}\n")
if self.next_handler:
return self.next_handler.handle(log)
return "Logged"
# Chain: Console → File
console = ConsoleLogger()
file_logger = FileLogger()
console.set_next(file_logger)
console.handle({"level": 10, "message": "Debugging..."}) # Console only
console.handle({"level": 20, "message": "User logged in"}) # Console + File
Middleware in Web Frameworks
Web frameworks (Django, Flask) use middleware chains to process requests/responses (e.g., authentication, CORS, logging). Each middleware can modify the request, pass it along, or return a response early.
class AuthMiddleware:
def __init__(self, next_middleware=None):
self.next_middleware = next_middleware
def handle(self, request):
if not request.get("authenticated"):
return {"status": 401, "message": "Unauthorized"}
if self.next_middleware:
return self.next_middleware.handle(request)
return {"status": 200}
class LoggingMiddleware:
def handle(self, request):
print(f"Logging request: {request}")
if self.next_middleware:
return self.next_middleware.handle(request)
return {"status": 200}
# Chain: Logging → Auth
logging = LoggingMiddleware()
auth = AuthMiddleware()
logging.set_next(auth)
# Request with auth
print(logging.handle({"authenticated": True})) # 200
# Request without auth
print(logging.handle({"authenticated": False})) # 401
Best Practices
- Single Responsibility: Each handler should focus on one type of request (e.g., don’t let Level 1 handle both password resets and network issues).
- Default Handler: Always end the chain with a “catch-all” handler to avoid unprocessed requests (like our
SpecialistSupport). - Limit Chain Length: Avoid deep chains to prevent performance bottlenecks.
- Test Thoroughly: Verify all request types are processed by the correct handler, and unhandled cases are caught.
- Document Handler Order: Clearly document the chain order (e.g., “Level 1 → Level 2 → Specialist”) to avoid confusion.
Conclusion
The Chain of Responsibility pattern is a powerful tool for simplifying request-handling workflows where multiple objects may process a request. By decoupling senders and receivers, it makes code more flexible, maintainable, and easier to extend.
Whether you’re building a support ticket system, validation pipeline, or logging framework, this pattern helps replace messy conditionals with a clean, modular chain of handlers. Next time you find yourself writing nested if-elif checks, consider using the Chain of Responsibility to level up your code!
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Python ABC Module Documentation
- Real Python: Design Patterns in Python
- Logging HOWTO (Python)