Chain of Responsibility Pattern Explained
TL;DR
The Chain of Responsibility pattern passes a request along a chain of handlers until one handles it. Each handler decides whether to process the request or pass it to the next handler. This decouples senders from receivers and allows multiple objects a chance to handle the request.
Core Concept
What is Chain of Responsibility?
The Chain of Responsibility pattern 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.
Core Components
The pattern consists of three main parts:
- Handler Interface: Defines the interface for handling requests and optionally storing the next handler
- Concrete Handlers: Implement the handling logic and decide whether to process or forward
- Client: Initiates the request to the first handler in the chain
How It Works
Each handler in the chain has a reference to the next handler. When a request arrives, the handler either:
- Processes it completely and stops the chain
- Processes it partially and passes it forward
- Doesn’t process it at all and passes it forward
- Processes it and still passes it forward (less common)
The chain can be dynamic — handlers can be added, removed, or reordered at runtime.
Why Use This Pattern?
Decoupling: The sender doesn’t need to know which handler will ultimately process the request. This reduces coupling between request senders and receivers.
Flexibility: You can add or remove handlers without changing existing code. The order of handlers can be changed easily.
Single Responsibility: Each handler focuses on one type of processing, making the code more maintainable.
Real-World Analogies
Think of a customer support system: a question first goes to a chatbot, then to a junior support agent, then to a senior agent, and finally to a specialist. Each level tries to answer, and if they can’t, they escalate to the next level.
Other examples include middleware in web frameworks, event bubbling in UI systems, and approval workflows in organizations.
Visual Guide
Chain of Responsibility Structure
classDiagram
class Handler {
<<abstract>>
+Handler next_handler
+set_next(handler) Handler
+handle(request)* void
}
class ConcreteHandlerA {
+handle(request) void
}
class ConcreteHandlerB {
+handle(request) void
}
class ConcreteHandlerC {
+handle(request) void
}
class Client
Handler <|-- ConcreteHandlerA
Handler <|-- ConcreteHandlerB
Handler <|-- ConcreteHandlerC
Handler o-- Handler : next
Client --> Handler
The Handler interface defines the chain structure. Each concrete handler can process the request or pass it to the next handler.
Request Flow Through Chain
sequenceDiagram
participant Client
participant HandlerA
participant HandlerB
participant HandlerC
Client->>HandlerA: handle(request)
alt Can handle
HandlerA->>HandlerA: Process request
HandlerA-->>Client: Done
else Cannot handle
HandlerA->>HandlerB: handle(request)
alt Can handle
HandlerB->>HandlerB: Process request
HandlerB-->>Client: Done
else Cannot handle
HandlerB->>HandlerC: handle(request)
HandlerC->>HandlerC: Process request
HandlerC-->>Client: Done
end
end
The request flows through the chain until a handler processes it. If no handler can process it, the request may go unhandled (or a default handler catches it).
Examples
Example 1: Support Ticket System
Let’s build a customer support system where tickets are routed based on priority.
from abc import ABC, abstractmethod
from enum import Enum
class Priority(Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
CRITICAL = 4
class SupportTicket:
def __init__(self, priority: Priority, description: str):
self.priority = priority
self.description = description
# Handler Interface
class SupportHandler(ABC):
def __init__(self):
self._next_handler = None
def set_next(self, handler):
"""Set the next handler in the chain."""
self._next_handler = handler
return handler # Return for chaining: h1.set_next(h2).set_next(h3)
@abstractmethod
def handle(self, ticket: SupportTicket):
"""Handle the ticket or pass to next handler."""
pass
# Concrete Handlers
class JuniorSupport(SupportHandler):
def handle(self, ticket: SupportTicket):
if ticket.priority == Priority.LOW:
print(f"Junior Support: Handling '{ticket.description}'")
return True
elif self._next_handler:
return self._next_handler.handle(ticket)
return False
class SeniorSupport(SupportHandler):
def handle(self, ticket: SupportTicket):
if ticket.priority in [Priority.MEDIUM, Priority.HIGH]:
print(f"Senior Support: Handling '{ticket.description}'")
return True
elif self._next_handler:
return self._next_handler.handle(ticket)
return False
class Manager(SupportHandler):
def handle(self, ticket: SupportTicket):
if ticket.priority == Priority.CRITICAL:
print(f"Manager: Handling '{ticket.description}'")
return True
elif self._next_handler:
return self._next_handler.handle(ticket)
print(f"No one could handle: '{ticket.description}'")
return False
# Client code
if __name__ == "__main__":
# Build the chain
junior = JuniorSupport()
senior = SeniorSupport()
manager = Manager()
junior.set_next(senior).set_next(manager)
# Create tickets
tickets = [
SupportTicket(Priority.LOW, "Password reset"),
SupportTicket(Priority.HIGH, "Server down"),
SupportTicket(Priority.CRITICAL, "Data breach"),
SupportTicket(Priority.MEDIUM, "Feature request")
]
# Process tickets
for ticket in tickets:
print(f"\nProcessing: {ticket.description} (Priority: {ticket.priority.name})")
junior.handle(ticket)
Expected Output:
Processing: Password reset (Priority: LOW)
Junior Support: Handling 'Password reset'
Processing: Server down (Priority: HIGH)
Senior Support: Handling 'Server down'
Processing: Data breach (Priority: CRITICAL)
Manager: Handling 'Data breach'
Processing: Feature request (Priority: MEDIUM)
Senior Support: Handling 'Feature request'
Try it yourself: Add a new handler for “TeamLead” that handles HIGH priority tickets, and modify SeniorSupport to only handle MEDIUM priority.
Example 2: Authentication Middleware Chain
Web frameworks often use chain of responsibility for request processing. Here’s a simplified authentication system:
from abc import ABC, abstractmethod
from typing import Optional
class Request:
def __init__(self, username: str, password: str, role: str = None):
self.username = username
self.password = password
self.role = role
self.authenticated = False
class Middleware(ABC):
def __init__(self):
self._next: Optional[Middleware] = None
def set_next(self, middleware):
self._next = middleware
return middleware
@abstractmethod
def check(self, request: Request) -> bool:
"""Return True if request should continue, False to stop."""
pass
class AuthenticationMiddleware(Middleware):
def __init__(self, valid_users: dict):
super().__init__()
self._valid_users = valid_users # {username: password}
def check(self, request: Request) -> bool:
print(f"[Auth] Checking credentials for {request.username}...")
if request.username not in self._valid_users:
print(f"[Auth] ❌ User not found")
return False
if self._valid_users[request.username] != request.password:
print(f"[Auth] ❌ Invalid password")
return False
print(f"[Auth] ✓ Authenticated")
request.authenticated = True
if self._next:
return self._next.check(request)
return True
class RoleCheckMiddleware(Middleware):
def __init__(self, required_role: str):
super().__init__()
self._required_role = required_role
def check(self, request: Request) -> bool:
print(f"[Role] Checking if user has '{self._required_role}' role...")
if request.role != self._required_role:
print(f"[Role] ❌ Insufficient permissions")
return False
print(f"[Role] ✓ Role verified")
if self._next:
return self._next.check(request)
return True
class RateLimitMiddleware(Middleware):
def __init__(self, max_requests: int):
super().__init__()
self._max_requests = max_requests
self._request_count = {}
def check(self, request: Request) -> bool:
print(f"[RateLimit] Checking request count...")
count = self._request_count.get(request.username, 0)
if count >= self._max_requests:
print(f"[RateLimit] ❌ Too many requests ({count}/{self._max_requests})")
return False
self._request_count[request.username] = count + 1
print(f"[RateLimit] ✓ Request allowed ({count + 1}/{self._max_requests})")
if self._next:
return self._next.check(request)
return True
# Client code
if __name__ == "__main__":
# Setup
users = {"alice": "pass123", "bob": "secret456"}
auth = AuthenticationMiddleware(users)
rate_limit = RateLimitMiddleware(max_requests=3)
role_check = RoleCheckMiddleware(required_role="admin")
# Build chain: auth -> rate_limit -> role_check
auth.set_next(rate_limit).set_next(role_check)
# Test cases
print("=== Test 1: Valid admin user ===")
req1 = Request("alice", "pass123", "admin")
result = auth.check(req1)
print(f"Result: {'✓ Allowed' if result else '❌ Denied'}\n")
print("=== Test 2: Wrong password ===")
req2 = Request("alice", "wrong", "admin")
result = auth.check(req2)
print(f"Result: {'✓ Allowed' if result else '❌ Denied'}\n")
print("=== Test 3: Non-admin user ===")
req3 = Request("bob", "secret456", "user")
result = auth.check(req3)
print(f"Result: {'✓ Allowed' if result else '❌ Denied'}\n")
print("=== Test 4: Rate limit (4th request) ===")
for i in range(4):
print(f"\n--- Request {i+1} ---")
req = Request("alice", "pass123", "admin")
result = auth.check(req)
print(f"Result: {'✓ Allowed' if result else '❌ Denied'}")
Expected Output:
=== Test 1: Valid admin user ===
[Auth] Checking credentials for alice...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ✓ Request allowed (1/3)
[Role] Checking if user has 'admin' role...
[Role] ✓ Role verified
Result: ✓ Allowed
=== Test 2: Wrong password ===
[Auth] Checking credentials for alice...
[Auth] ❌ Invalid password
Result: ❌ Denied
=== Test 3: Non-admin user ===
[Auth] Checking credentials for bob...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ✓ Request allowed (1/3)
[Role] Checking if user has 'admin' role...
[Role] ❌ Insufficient permissions
Result: ❌ Denied
=== Test 4: Rate limit (4th request) ===
--- Request 1 ---
[Auth] Checking credentials for alice...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ✓ Request allowed (2/3)
[Role] Checking if user has 'admin' role...
[Role] ✓ Role verified
Result: ✓ Allowed
--- Request 2 ---
[Auth] Checking credentials for alice...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ✓ Request allowed (3/3)
[Role] Checking if user has 'admin' role...
[Role] ✓ Role verified
Result: ✓ Allowed
--- Request 3 ---
[Auth] Checking credentials for alice...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ❌ Too many requests (3/3)
Result: ❌ Denied
--- Request 4 ---
[Auth] Checking credentials for alice...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ❌ Too many requests (3/3)
Result: ❌ Denied
Try it yourself: Add a LoggingMiddleware that logs all requests before authentication. Place it at the start of the chain.
Java/C++ Notes
Java: Use interfaces or abstract classes for the Handler. The pattern looks similar:
public abstract class Handler {
private Handler next;
public Handler setNext(Handler next) {
this.next = next;
return next;
}
public abstract boolean handle(Request request);
}
C++: Use virtual functions and pointers:
class Handler {
protected:
Handler* next = nullptr;
public:
Handler* setNext(Handler* handler) {
next = handler;
return handler;
}
virtual bool handle(Request& request) = 0;
virtual ~Handler() = default;
};
Common Mistakes
1. Breaking the Chain Accidentally
Mistake: Forgetting to call the next handler, which breaks the chain.
class BadHandler(SupportHandler):
def handle(self, ticket):
if ticket.priority == Priority.LOW:
print("Handling low priority")
return True
# Forgot to call self._next_handler.handle(ticket)!
return False
Why it’s wrong: Requests that this handler can’t process will be dropped instead of forwarded.
Fix: Always check if there’s a next handler and call it:
if self._next_handler:
return self._next_handler.handle(ticket)
2. Not Checking for None Before Calling Next
Mistake: Calling self._next_handler.handle() without checking if _next_handler is None.
def handle(self, request):
if not self.can_handle(request):
return self._next_handler.handle(request) # May be None!
Why it’s wrong: This causes an AttributeError when the handler is the last in the chain.
Fix: Always check:
if self._next_handler:
return self._next_handler.handle(request)
return False # Or handle the "no handler found" case
3. Creating Circular Chains
Mistake: Accidentally creating a loop in the chain.
handler1.set_next(handler2)
handler2.set_next(handler3)
handler3.set_next(handler1) # Creates a cycle!
Why it’s wrong: Requests will loop infinitely, causing stack overflow or infinite loops.
Fix: Be careful when building chains. Consider adding cycle detection in development:
def set_next(self, handler):
# Check for cycles (optional, for debugging)
current = handler
while current:
if current is self:
raise ValueError("Circular chain detected!")
current = current._next_handler
self._next_handler = handler
return handler
4. Handlers Doing Too Much
Mistake: Making handlers responsible for multiple unrelated concerns.
class OverloadedHandler(Handler):
def handle(self, request):
# Validates request
# Logs request
# Checks permissions
# Processes business logic
# Sends notifications
# All in one handler!
Why it’s wrong: Violates Single Responsibility Principle and makes the chain less flexible.
Fix: Split into multiple focused handlers:
validation = ValidationHandler()
logging = LoggingHandler()
auth = AuthorizationHandler()
processor = ProcessingHandler()
validation.set_next(logging).set_next(auth).set_next(processor)
5. Not Handling the “No Handler Found” Case
Mistake: Not having a default handler or fallback when no handler in the chain can process the request.
# Request goes through entire chain but no one handles it
# Silently fails or returns False with no logging
Why it’s wrong: Makes debugging difficult and can lead to silent failures.
Fix: Add a default handler at the end of the chain:
class DefaultHandler(Handler):
def handle(self, request):
print(f"Warning: No handler found for request: {request}")
# Log, raise exception, or return default response
return False
# Add to end of chain
handler1.set_next(handler2).set_next(handler3).set_next(DefaultHandler())
Interview Tips
When to Mention Chain of Responsibility
In interviews, bring up this pattern when you hear:
- “Multiple objects might handle a request”
- “Processing pipeline” or “middleware”
- “Request routing” or “escalation”
- “Validation chain” or “filter chain”
Key Points to Emphasize
1. Decoupling sender and receiver: Explain that the client doesn’t need to know which handler will process the request. Say: “The sender is decoupled from receivers — it just sends to the first handler, and the chain figures out who handles it.”
2. Runtime flexibility: Mention that you can add, remove, or reorder handlers at runtime without changing client code.
3. Single Responsibility: Each handler has one clear job. This makes the system easier to test and maintain.
Common Interview Questions
Q: “What’s the difference between Chain of Responsibility and Decorator?”
A: “Both chain objects together, but they have different purposes. Chain of Responsibility passes a request along until ONE handler processes it (or none do). Decorator wraps an object to ADD behavior — all decorators execute. In CoR, only one handler typically processes the request. In Decorator, all wrappers add their behavior.”
Q: “What if no handler can process the request?”
A: “You have several options: (1) Add a default handler at the end of the chain that handles all unprocessed requests, (2) Return a failure status or None, (3) Throw an exception. The choice depends on whether an unhandled request is an error condition or an expected possibility.”
Q: “When would you NOT use this pattern?”
A: “Don’t use it when: (1) You know exactly which object should handle the request — just call it directly, (2) The order of processing doesn’t matter and all handlers should process the request — use Observer pattern instead, (3) Performance is critical and the chain is very long — the overhead of traversing the chain might be significant.”
Code Challenge Preparation
Be ready to implement a chain in 10-15 minutes. Practice these scenarios:
- Logging system with different log levels (DEBUG, INFO, WARN, ERROR)
- Expense approval workflow (Manager → Director → VP → CEO based on amount)
- Input validation chain (NotNull → Format → Range → Business Rules)
What Interviewers Look For
- Proper abstraction: Do you create a base Handler class/interface?
- Chain building: Can you fluently build the chain with
set_next()returning the handler? - Edge cases: Do you handle the last handler in the chain (checking for None)?
- Flexibility: Can you explain how to modify the chain at runtime?
Pro Tip
When explaining the pattern, draw a simple diagram showing the chain flow. Interviewers appreciate visual thinkers. Say: “Let me sketch the flow…” and draw:
Client → HandlerA → HandlerB → HandlerC → DefaultHandler
↓ ↓ ↓
Can't Can't Handles!
handle handle
This shows you understand the pattern deeply and can communicate it clearly.
Key Takeaways
- Chain of Responsibility passes requests along a chain of handlers, where each handler decides to process or forward the request, decoupling senders from receivers
- The pattern consists of a Handler interface with a reference to the next handler, Concrete Handlers that implement processing logic, and a Client that initiates requests
- Use this pattern for request routing, validation pipelines, middleware systems, and approval workflows where multiple objects might handle a request
- Always check if
next_handlerexists before calling it, avoid circular chains, and consider adding a default handler at the end to catch unprocessed requests - The pattern provides runtime flexibility — you can add, remove, or reorder handlers without changing client code, making it ideal for configurable processing pipelines