🔗 Chain of Responsibility Design Pattern
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
🌍 Real-World Analogy
Analogy — Customer Support Escalation
When you call customer support, you first speak to a Level 1 agent. If they can't solve your problem, they escalate to Level 2. If that fails, it goes to a manager, then to engineering. Each handler in the chain either resolves the issue or passes it up. You don't decide who handles it — the chain does.
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
U["😤 Customer"] -->|"calls"| L1["🎧 Level 1"]
L1 -->|"escalates"| L2["👔 Level 2"]
L2 -->|"escalates"| M["🧑💼 Manager"]
M -->|"escalates"| E["👨💻 Engineering"]
E -->|"resolves"| S(["✅ Solved!"])
style U fill:#FCE4EC,stroke:#C62828,stroke-width:2px,color:#000
style L1 fill:#FFF3E0,stroke:#E65100,color:#000
style L2 fill:#FFF3E0,stroke:#E65100,color:#000
style M fill:#E3F2FD,stroke:#1565C0,color:#000
style E fill:#E3F2FD,stroke:#1565C0,color:#000
style S fill:#E8F5E9,stroke:#2E7D32,color:#000 🏗️ Pattern Structure
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
Client["👤 Client"] -->|"sends request"| Handler[["🔗 Handler"]]
Handler -.->|"implements"| HandlerA{{"🟣 HandlerA"}}
Handler -.->|"implements"| HandlerB{{"🟪 HandlerB"}}
HandlerA -->|"passes to"| HandlerB
HandlerB -->|"passes to"| HandlerC{{"🟣 HandlerC"}}
style Client fill:#E8F5E9,stroke:#2E7D32,color:#000
style Handler fill:#FFF3E0,stroke:#E65100,color:#000
style HandlerA fill:#E3F2FD,stroke:#1565C0,color:#000
style HandlerB fill:#E3F2FD,stroke:#1565C0,color:#000
style HandlerC fill:#E3F2FD,stroke:#1565C0,color:#000 UML Class Diagram
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
class SupportHandler {
<<abstract>>
-next: SupportHandler
+setNext(SupportHandler next) SupportHandler
+handle(SupportTicket ticket) void
#canHandle(SupportTicket ticket)* boolean
#process(SupportTicket ticket)* void
}
class FAQHandler {
#canHandle(SupportTicket ticket) boolean
#process(SupportTicket ticket) void
}
class TechSupportHandler {
#canHandle(SupportTicket ticket) boolean
#process(SupportTicket ticket) void
}
class EngineeringHandler {
#canHandle(SupportTicket ticket) boolean
#process(SupportTicket ticket) void
}
class SupportTicket {
-issue: String
-priority: Priority
+getIssue() String
+getPriority() Priority
}
SupportHandler --> SupportHandler : next
FAQHandler --|> SupportHandler
TechSupportHandler --|> SupportHandler
EngineeringHandler --|> SupportHandler
SupportHandler ..> SupportTicket : handles ❓ The Problem
You have a request that could be handled by multiple handlers, but:
- The handler is not known at compile time
- You don't want the sender to be coupled to a specific handler
- Multiple handlers may need to process the same request (pipeline)
- The set of handlers and their order may change at runtime
Example: An HTTP request passes through authentication, authorization, rate limiting, logging, and validation — each middleware can stop or pass the request forward.
Without This Pattern
public class RequestProcessor {
public void handleRequest(HttpRequest request) {
// All handling logic in ONE monolithic method
if (request.getHeader("Authorization") == null) {
throw new UnauthorizedException("No token");
}
if (rateLimitExceeded(request.getRemoteAddr())) {
throw new TooManyRequestsException();
}
if (!isValidPayload(request.getBody())) {
throw new BadRequestException("Invalid payload");
}
log(request);
// Adding a new check? Modify THIS class. Reordering? Refactor THIS method.
processBusinessLogic(request);
}
}
- Monolithic method — all handling logic lives in one massive method; adding a new check bloats it further
- Violates Single Responsibility — authentication, rate limiting, validation, and logging are all in one class
- Cannot reorder or reconfigure dynamically — the processing order is hardcoded; different endpoints cannot have different filter chains
- Not reusable — the rate-limiting logic cannot be extracted and used in a different service without copy-paste
- Pain point: The team wants to skip authentication for health-check endpoints but apply it everywhere else. Without a chain, you add yet another
ifat the top of the method, creating a tangled nest of special cases
✅ The Solution
The Chain of Responsibility pattern organizes handlers into a linked chain:
- Each handler has a reference to the next handler in the chain
- Upon receiving a request, a handler decides to process it, pass it forward, or both
- The client only needs to send the request to the first handler
- Handlers can be reordered, added, or removed dynamically
💻 Implementation
// Base Handler
public abstract class SupportHandler {
private SupportHandler next;
public SupportHandler setNext(SupportHandler next) {
this.next = next;
return next; // enables fluent chaining
}
public void handle(SupportTicket ticket) {
if (canHandle(ticket)) {
process(ticket);
} else if (next != null) {
next.handle(ticket);
} else {
System.out.println("❌ No handler could process: " + ticket.getIssue());
}
}
protected abstract boolean canHandle(SupportTicket ticket);
protected abstract void process(SupportTicket ticket);
}
// Request object
public class SupportTicket {
public enum Priority { LOW, MEDIUM, HIGH, CRITICAL }
private final String issue;
private final Priority priority;
public SupportTicket(String issue, Priority priority) {
this.issue = issue;
this.priority = priority;
}
public String getIssue() { return issue; }
public Priority getPriority() { return priority; }
}
// Concrete Handlers
public class FAQHandler extends SupportHandler {
@Override
protected boolean canHandle(SupportTicket ticket) {
return ticket.getPriority() == SupportTicket.Priority.LOW;
}
@Override
protected void process(SupportTicket ticket) {
System.out.println("📖 FAQ Bot resolved: " + ticket.getIssue());
}
}
public class TechSupportHandler extends SupportHandler {
@Override
protected boolean canHandle(SupportTicket ticket) {
return ticket.getPriority() == SupportTicket.Priority.MEDIUM;
}
@Override
protected void process(SupportTicket ticket) {
System.out.println("🛠️ Tech Support resolved: " + ticket.getIssue());
}
}
public class EngineeringHandler extends SupportHandler {
@Override
protected boolean canHandle(SupportTicket ticket) {
return ticket.getPriority() == SupportTicket.Priority.HIGH ||
ticket.getPriority() == SupportTicket.Priority.CRITICAL;
}
@Override
protected void process(SupportTicket ticket) {
System.out.println("👨💻 Engineering team resolved: " + ticket.getIssue());
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Build the chain
SupportHandler chain = new FAQHandler();
chain.setNext(new TechSupportHandler())
.setNext(new EngineeringHandler());
// Send tickets
chain.handle(new SupportTicket("Reset password", SupportTicket.Priority.LOW));
chain.handle(new SupportTicket("App crashing", SupportTicket.Priority.MEDIUM));
chain.handle(new SupportTicket("Data corruption", SupportTicket.Priority.CRITICAL));
}
}
// Middleware-style chain — every handler processes and passes along
public interface Middleware {
boolean handle(HttpRequest request);
}
public abstract class BaseMiddleware implements Middleware {
private BaseMiddleware next;
public BaseMiddleware linkWith(BaseMiddleware next) {
this.next = next;
return next;
}
@Override
public boolean handle(HttpRequest request) {
if (!check(request)) {
return false; // Stop chain
}
return next == null || next.handle(request);
}
protected abstract boolean check(HttpRequest request);
}
public class AuthenticationMiddleware extends BaseMiddleware {
@Override
protected boolean check(HttpRequest request) {
if (request.getHeader("Authorization") == null) {
System.out.println("🚫 Auth failed: No token");
return false;
}
System.out.println("✅ Authenticated");
return true;
}
}
public class RateLimitMiddleware extends BaseMiddleware {
private final Map<String, Integer> requestCounts = new HashMap<>();
private static final int MAX_REQUESTS = 100;
@Override
protected boolean check(HttpRequest request) {
String ip = request.getRemoteAddr();
int count = requestCounts.getOrDefault(ip, 0) + 1;
requestCounts.put(ip, count);
if (count > MAX_REQUESTS) {
System.out.println("🚫 Rate limit exceeded for: " + ip);
return false;
}
System.out.println("✅ Rate limit OK");
return true;
}
}
public class LoggingMiddleware extends BaseMiddleware {
@Override
protected boolean check(HttpRequest request) {
System.out.println("📝 Logging: " + request.getMethod() + " " + request.getPath());
return true; // Always passes
}
}
// Build pipeline
BaseMiddleware chain = new LoggingMiddleware();
chain.linkWith(new AuthenticationMiddleware())
.linkWith(new RateLimitMiddleware());
🎯 When to Use
- When more than one object may handle a request and the handler isn't known a priori
- When you want to issue a request to one of several objects without specifying the receiver explicitly
- When the set of handlers should be configured dynamically
- When you need a middleware pipeline (authentication, logging, validation)
- When you want to decouple request senders from their processors
🏭 Real-World Examples
| Framework/Library | Usage |
|---|---|
| Java Servlet Filters | javax.servlet.FilterChain — classic chain of responsibility |
| Spring Security Filter Chain | Authentication and authorization filters in sequence |
| Spring Interceptors | HandlerInterceptor pre/post processing |
Java try-catch blocks | Exception handling is a chain — first matching catch wins |
| Java Logging | java.util.logging.Logger parent chain |
| Apache Commons Chain | Explicit Chain of Responsibility framework |
| Netty ChannelPipeline | Network event processing chain |
⚠️ Pitfalls
Common Mistakes
- Unhandled requests — If no handler processes the request, it silently disappears. Always have a fallback handler at the end.
- Performance — Long chains with many handlers add latency. Profile if chains grow large.
- Debugging difficulty — Hard to trace which handler processed (or dropped) a request. Add logging.
- Circular chains — Accidentally linking handlers in a circle causes infinite loops.
- Guaranteed handling — If a request must be handled, validate the chain at construction time.
📝 Key Takeaways
Summary
- Chain of Responsibility decouples senders from receivers by passing requests along a chain
- Two flavors: stop-on-handle (first capable handler processes) or pipeline (all handlers process)
- Handlers are linked at runtime — easy to add, remove, or reorder
- The pattern is the backbone of servlet filters, middleware stacks, and event bubbling
- Follows Single Responsibility Principle — each handler does one thing
- Combine with Command pattern to queue and chain commands