Chain of Responsibility Pattern — Passing Requests Along a Chain
What You'll Learn
You will learn how the Chain of Responsibility pattern builds a pipeline of handlers, each deciding whether to Process a request or pass it to the next handler. You will see how this decouples request senders from receivers.
Why It Matters
Hardcoding if-else chains for request handling makes systems rigid and hard to extend. Every new handler requires modifying existing code. Chain of Responsibility lets you add, remove, or reorder handlers without touching the rest of the chain. Consider an HTTP request filter: without the chain, adding a new CORS check means inserting an if-block into existing filtering code. With the chain, you write a new handler class and insert it into the pipeline configuration.
Real-World Use
Web frameworks route HTTP requests through middleware layers — authentication, logging, Rate Limiting, CORS — each either handling the request or passing it along. DodaTech's CLI command pipeline uses Chain of Responsibility to validate flags, load config files, and execute subcommands in sequence. Adding a new validation step means writing one handler and inserting it into the chain — no existing code changes.
The Pattern
Handler declares an interface for processing requests and optionally holding a reference to the next handler. ConcreteHandler decides whether to handle or forward.
from abc import ABC, abstractmethod
class LogHandler(ABC):
def __init__(self):
self._next = None
def set_next(self, handler: "LogHandler") -> "LogHandler":
self._next = handler
return handler
@abstractmethod
def handle(self, level: str, message: str) -> str:
pass
class DebugHandler(LogHandler):
def handle(self, level: str, message: str) -> str:
if level == "DEBUG":
return f"DEBUG: {message}"
if self._next:
return self._next.handle(level, message)
return ""
class InfoHandler(LogHandler):
def handle(self, level: str, message: str) -> str:
if level == "INFO":
return f"INFO: {message}"
if self._next:
return self._next.handle(level, message)
return ""
class ErrorHandler(LogHandler):
def handle(self, level: str, message: str) -> str:
if level == "ERROR":
return f"ERROR: {message}"
if self._next:
return self._next.handle(level, message)
return ""
chain = DebugHandler()
chain.set_next(InfoHandler()).set_next(ErrorHandler())
for msg_level, msg in [("DEBUG", "starting up"), ("INFO", "config loaded"), ("ERROR", "disk full"), ("WARN", "low memory")]:
result = chain.handle(msg_level, msg)
if result:
print(result)
else:
print(f"Unhandled: {msg_level} {msg}")
DEBUG: starting up
INFO: config loaded
ERROR: disk full
Unhandled: WARN low memory
Structure
classDiagram
class Handler {
<>
+setNext(handler): Handler
+handle(request)
}
class ConcreteHandler1 {
+handle(request)
}
class ConcreteHandler2 {
+handle(request)
}
class ConcreteHandler3 {
+handle(request)
}
ConcreteHandler1 ..|> Handler
ConcreteHandler2 ..|> Handler
ConcreteHandler3 ..|> Handler
ConcreteHandler1 --> ConcreteHandler2 : next
ConcreteHandler2 --> ConcreteHandler3 : next
Real-World Usage
- ASP.NET Core Middleware — HTTP requests flow through a pipeline of middleware delegates that can handle early (auth) or pass through.
- Java servlet filters —
javax.servlet.Filterchains implement logging, compression, and authentication. - JavaScript event bubbling — DOM events propagate from target element up to document root, with each handler able to stop propagation.
- Unix pipelines —
grep | sort | uniqis a chain where each command processes and forwards data.
Related Patterns
- Command encapsulates requests; Chain of Responsibility routes them.
- Composite has a similar tree-like structure but focuses on part-whole hierarchies.
- Decorator wraps behaviour around an object; Chain passes behaviour along a sequence.
- Observer broadcasts to many; Chain delivers to exactly one or until handled.
Pros and Cons
| Pros | Cons |
|---|---|
| Decouples sender and receiver | No guarantee a request will be handled |
| Easy to add, remove, or reorder handlers | Can be hard to debug the path a request takes |
| Follows the Single Responsibility Principle | Handler chains can become long and slow |
| Runtime flexibility in processing order | Request-specific context must be passed explicitly |
The logging chain demonstrates the essential flow: each handler checks whether it can Process the request. If yes, it returns a result and the chain stops. If no, it forwards to the next handler. The set_next method returns the next handler to enable fluent chaining: DebugHandler().set_next(InfoHandler()).set_next(ErrorHandler()).
Practice Questions
- How would you implement a break mechanism that stops the chain after a handler produces a result?
- What are the testing strategies for an individual handler versus the entire chain?
- Implement a chain that validates user input through sanitisation, formatting, authorisation, and persistence steps.
- Compare Chain of Responsibility with a simple list of callbacks — what does the pattern provide that callbacks do not?
Challenge
Implement a chain where handlers can modify the request before passing it along (like middleware that adds headers to an HTTP request). Each handler should both Process and forward.
Real-World Task
Map your application's request validation or preprocessing pipeline to a Chain of Responsibility. Use DodaTech's pipeline debugger to trace how requests flow through each handler.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro