Low-Level Design (LLD): Chain of Responsibility Pattern
The Chain of Responsibility is a behavioral design pattern that decouples the sender of a request from its receivers by giving multiple objects a chance to handle the request. Requests are passed sequentially along a chain of handlers until one of them processes it or the end of the chain is reached.
1. Problem Statement: The Pitfalls of Monolithic Control Flow
In typical software systems, business requests often require conditional multi-tier authorization or processing logic. Consider an enterprise employee leave approval feature where the number of requested leave days determines who can approve the request:
- 1 to 3 Days: Processed by the immediate Supervisor.
- 4 to 7 Days: Processed by the Manager.
- 8 to 14 Days: Processed by the Director.
- 15+ Days: Processed by HR or rejected.
The Naive Implementation
A beginner or naive approach wraps this business logic inside a monolithic class using deeply nested if-else or switch-case branches:
public class LeaveApprovalService {
public void processLeaveRequest(String employeeName, int leaveDays) {
if (leaveDays <= 3) {
// Supervisor execution logic
System.out.println("Supervisor approved " + leaveDays + " days of leave for " + employeeName);
} else if (leaveDays <= 7) {
// Manager execution logic
System.out.println("Manager approved " + leaveDays + " days of leave for " + employeeName);
} else if (leaveDays <= 14) {
// Director execution logic
System.out.println("Director approved " + leaveDays + " days of leave for " + employeeName);
} else {
// Rejection logic
System.out.println("Leave request denied for " + employeeName);
}
}
}
Critical Architectural Red Flags
- Violation of Single Responsibility Principle (SRP): The
LeaveApprovalServiceclass knows the inner workings, thresholds, and operational tasks of supervisors, managers, directors, and HR. - Violation of Open/Closed Principle (OCP): Adding a new approval tier (e.g., Senior Vice President) or altering existing approval logic requires modifying the core application service, risking regression errors.
- Bloated Logic and Poor Maintainability: Real-world approval steps involve complex business logic (e.g., audit logging, database updates, background checks) rather than single print statements. Embedding all handler steps in one place results in thousands of lines of unmaintainable, tightly coupled code.
- Lack of Dynamic Flexibility: You cannot reconfigure, reorder, or swap approvers at runtime without changing source code and re-deploying the application.
2. Core Mental Model & Real-World Analogies
To understand how the Chain of Responsibility pattern resolves these problems, consider two real-world analogies:
The Relay Race Analogy
In a relay race, runner A carries the baton and runs their leg of the race. Runner A then passes the baton to runner B, who passes it to runner C. The request (the baton) flows from person to person along a designated track until the final runner crosses the finish line.
Real-Life Corporate Approval Workflow
When an employee submits a leave request, they do not manually select every manager, director, or executive who needs to review it. The employee simply submits the ticket to their primary supervisor. If the supervisor lacks the authority to approve a 10-day leave, the ticket automatically escalates up the management chain to someone with the appropriate clearance.
3. Chain of Responsibility vs. State Pattern
It is common to confuse the Chain of Responsibility Pattern with the State Pattern, as both delegate requests to secondary objects. However, their intent and mechanics differ fundamentally:
4. Architectural Design: Abstract Class vs. Interface
When implementing the Chain of Responsibility, handlers can be structured using either an interface or an abstract class.
Note: An abstract class is preferred for this pattern because every node in the chain needs to maintain a reference to the next handler (
nextApprover) and expose a method to configure it (setNextApprover()). Using an abstract class prevents code duplication across concrete handlers.
5. Step-by-Step Code Implementation
Here is a complete, production-ready implementation in Java demonstrating concrete handoffs across tiers.
Step 1: Base Handler (Approver Abstract Class)
public abstract class Approver {
protected Approver nextApprover;
// Sets the next link in the responsibility chain
public void setNextApprover(Approver nextApprover) {
this.nextApprover = nextApprover;
}
// Abstract method to be overridden by concrete handlers
public abstract void processLeaveRequest(int leaveDays);
}
Step 2: Tier 1 Handler (Supervisor)
public class Supervisor extends Approver {
@Override
public void processLeaveRequest(int leaveDays) {
if (leaveDays <= 3) {
// Task execution specific to Supervisor
System.out.println("Supervisor: Verified team coverage. Approved " + leaveDays + " day(s) of leave.");
} else if (nextApprover != null) {
// Pass request to the next handler in the chain
System.out.println("Supervisor: Escalating request (> 3 days)...");
nextApprover.processLeaveRequest(leaveDays);
} else {
System.out.println("Supervisor: Request denied. No further approver in chain.");
}
}
}
Step 3: Tier 2 Handler (Manager)
public class Manager extends Approver {
@Override
public void processLeaveRequest(int leaveDays) {
if (leaveDays <= 7) {
// Task execution specific to Manager
System.out.println("Manager: Checked project milestones. Approved " + leaveDays + " day(s) of leave.");
} else if (nextApprover != null) {
// Pass request to the next handler in the chain
System.out.println("Manager: Escalating request (> 7 days)...");
nextApprover.processLeaveRequest(leaveDays);
} else {
System.out.println("Manager: Request denied. No further approver in chain.");
}
}
}
Step 4: Tier 3 Handler (Director)
public class Director extends Approver {
@Override
public void processLeaveRequest(int leaveDays) {
if (leaveDays <= 14) {
// Task execution specific to Director
System.out.println("Director: Reviewed resource budget. Approved " + leaveDays + " day(s) of leave.");
} else if (nextApprover != null) {
// Pass request to the next handler in the chain
System.out.println("Director: Escalating request (> 14 days)...");
nextApprover.processLeaveRequest(leaveDays);
} else {
System.out.println("Director: Request denied. Maximum threshold exceeded.");
}
}
}
6. Complete Flow Walkthrough and Client Execution
The client code wires the individual handlers together into a pipeline and dispatches a request to the first node (Supervisor).
public class Application {
public static void main(String[] args) {
// Instantiate handlers
Approver supervisor = new Supervisor();
Approver manager = new Manager();
Approver director = new Director();
// Construct the chain: Supervisor -> Manager -> Director
supervisor.setNextApprover(manager);
manager.setNextApprover(director);
// Test Scenario 1: Short leave request (Handled by Supervisor)
System.out.println("--- Submitting 2 Days Leave ---");
supervisor.processLeaveRequest(2);
// Test Scenario 2: Moderate leave request (Escalates to Manager)
System.out.println("\n--- Submitting 5 Days Leave ---");
supervisor.processLeaveRequest(5);
// Test Scenario 3: Extended leave request (Escalates to Director)
System.out.println("\n--- Submitting 10 Days Leave ---");
supervisor.processLeaveRequest(10);
// Test Scenario 4: Extreme leave request (Reaches end of chain and is denied)
System.out.println("\n--- Submitting 20 Days Leave ---");
supervisor.processLeaveRequest(20);
}
}
Runtime Execution Flow Diagram
7. Dynamic Extensibility: Adding New Handlers Zero Friction
To show how the Chain of Responsibility pattern adheres to the Open/Closed Principle, suppose corporate policy introduces a new HR approval tier for requests exceeding 14 days.
Step 1: Create the New HR Handler Class
public class HR extends Approver {
@Override
public void processLeaveRequest(int leaveDays) {
if (leaveDays <= 30) {
System.out.println("HR: Processed sabbatical/extended leaves. Approved " + leaveDays + " day(s) of leave.");
} else if (nextApprover != null) {
nextApprover.processLeaveRequest(leaveDays);
} else {
System.out.println("HR: Leave denied. Exceeds max policy limit of 30 days.");
}
}
}
Step 2: Plug the New Handler into the Chain
None of the existing classes (Supervisor, Manager, or Director) need to be modified. You simply update the chain setup in the client code:
Approver supervisor = new Supervisor();
Approver manager = new Manager();
Approver director = new Director();
Approver hr = new HR(); // New handler
// Reconfigure chain: Supervisor -> Manager -> Director -> HR
supervisor.setNextApprover(manager);
manager.setNextApprover(director);
director.setNextApprover(hr); // Plug HR at the end of the director step
// Request 20 days: Will escalate through Supervisor -> Manager -> Director -> HR
supervisor.processLeaveRequest(20);
8. Real-World Applications & Edge Cases
Real-World Production Use Cases
- Multi-Tier Customer Support Systems:
- Level 1 Support: AI Bot / Basic FAQ checks.
- Level 2 Support: Support Engineer handles standard technical issues.
- Level 3 Support: Senior Technical Lead / Manager handles systemic outages.
- Web Framework Request Filtering & Middleware:
AuthenticationFilter->RoleAuthorizationFilter->CSRFValidationFilter->RateLimitingFilter->Servlet Controller.
- Enterprise Logging Frameworks:
- Logger chains (
INFO->DEBUG->ERROR->CRITICAL_ALERT). Each handler processes log entries matching its assigned severity level.
- Logger chains (
Critical Edge Cases and Mitigation Strategies
- Unhandled Fallthrough: If no handler in the chain can process a request, it might reach the end of the chain without being handled. Always implement a default fallback step or check for
nextApprover != nullto handle unhandled requests gracefully. - Broken or Unlinked Chain: If a handler's
nextApproveris set incorrectly or forgotten during chain initialization, requests will drop unexpectedly. - Accidental Cycles: Avoid setting cyclic references (e.g.,
Director.next = Supervisor), which causeStackOverflowErrorexceptions due to infinite recursion.
Summary and Key Takeaways
- Decouples Sender and Handler: The client only needs to interact with the first handler in the chain (
supervisor.processLeaveRequest(...)), remaining completely decoupled from downstream processors. - Follows Single Responsibility Principle (SRP): Each handler class manages its own validation, business logic, and operational details independently.
- Follows Open/Closed Principle (OCP): New handlers (like
HR) can be added without changing existing concrete handler code. - Driven by Abstract Base Class: Using an abstract class (
Approver) lets concrete handlers inherit common properties likenextApproverandsetNextApprover()without code duplication. - Forwarding Mechanism: Unlike the cyclic State Pattern, the Chain of Responsibility pattern passes requests forward along a line until a capable handler processes them.
- Dynamic Runtime Configuration: Links in the chain can be added, removed, or re-ordered at runtime based on context or user permissions.
- Eliminates Code Smells: Replaces long, complex
if-elseand nested conditional blocks with clean, modular object-oriented classes. - Pipeline Fallthrough Protection: Always ensure the end of the chain handles unmatched requests cleanly to prevent unexpected failures.