Overview
Many business processes require multi-level approval workflows. These approval rules often change during system evolution. The Chain of Responsibility pattern addresses precisely this category of problems.
This pattern establishes that multiple objects have the opportunity to process a request, eliminating tight coupling between the sender and receiver. Objects are linked into a chain, and requests travel along this chain until one object handles it.
The core idea is to decompose request processing logic into granular handler classes. These handlers can be assembled in different sequences based on business requirements, forming a processing chain. If a request traverses the entire chain without being handled, a default fallback mechanism must be provided.
Implementation Structure
The pattern requires three key components:
- Abstract Handler: Defines the interface for handling requests and maintains a reference to the next handler in the chain
- Concrete Handlers: Implement specific processing logic for different scenarios
- Client: Builds and initiates the chain
Practical Example: Expense Approval System
Consider a meal expense approval workflow with three approval levels:
public abstract class ExpenseHandler {
protected ExpenseHandler nextApprover;
public void setNextApprover(ExpenseHandler nextApprover) {
this.nextApprover = nextApprover;
}
public abstract String processRequest(String applicant, double amount);
}
public class ProjectManagerHandler extends ExpenseHandler {
@Override
public String processRequest(String applicant, double amount) {
String result;
if (amount < 500) {
if ("Alex".equals(applicant)) {
result = "Project Manager approved " + applicant + "'s meal expense of $" + amount;
} else {
result = "Project Manager rejected " + applicant + "'s meal expense of $" + amount;
}
} else if (nextApprover != null) {
result = nextApprover.processRequest(applicant, amount);
} else {
result = "Request could not be processed - no appropriate approver found";
}
return result;
}
}
public class DepartmentManagerHandler extends ExpenseHandler {
@Override
public String processRequest(String applicant, double amount) {
String result;
if (amount < 1000) {
if ("Alex".equals(applicant)) {
result = "Department Manager approved " + applicant + "'s meal expense of $" + amount;
} else {
result = "Department Manager rejected " + applicant + "'s meal expense of $" + amount;
}
} else if (nextApprover != null) {
result = nextApprover.processRequest(applicant, amount);
} else {
result = "Request could not be processed - no appropriate approver found";
}
return result;
}
}
public class GeneralManagerHandler extends ExpenseHandler {
@Override
public String processRequest(String applicant, double amount) {
String result;
if (amount >= 1000) {
if ("Alex".equals(applicant)) {
result = "General Manager approved " + applicant + "'s meal expense of $" + amount;
} else {
result = "General Manager rejected " + applicant + "'s meal expense of $" + amount;
}
} else if (nextApprover != null) {
result = nextApprover.processRequest(applicant, amount);
} else {
result = "Request could not be processed - no appropriate approver found";
}
return result;
}
}
public class ApprovalClient {
public static void main(String[] args) {
ExpenseHandler level1 = new ProjectManagerHandler();
ExpenseHandler level2 = new DepartmentManagerHandler();
ExpenseHandler level3 = new GeneralManagerHandler();
level1.setNextApprover(level2);
level2.setNextApprover(level3);
String response1 = level1.processRequest("Michael", 600);
System.out.println(response1);
String response2 = level1.processRequest("William", 1800);
System.out.println(response2);
String response3 = level1.processRequest("Alex", 400);
System.out.println(response3);
}
}
// Output:
// Department Manager rejected Michael's meal expense of $600.0
// General Manager rejected William's meal expense of $1800.0
// Project Manager approved Alex's meal expense of $400.0
The recursive structure in this implementation allows requests to flow through the chain until an appropriate handler is found. Each handler decides whether to process the request or delegate it to the next handler in the chain.
Chain Variants
Beyond approval workflows, this pattern supprots functional chains where multiple processing capabilities are decomposed into single-responsibility classes. Handlers can be invoked selectively based on runtime requirements, providing greater flexibility and reusability.
Advantages and Disadvantages
Advantages:
- Single Responsibility: Each handler class encapsulates exactly one processing logic, improving code organization and maintainability
- Loose Coupling: Request senders have no knowledge of which handler will process the request, enabling independent evolution of handler chains
- Dynamic Composition: Handlers can be rearranged or extended without modifying existing code
Disadvantages:
- Handler Proliferation: Breaking down complex logic into granular handlers can result in numerous small classes
- Debugging Complexity: Request traversal through multiple handlers makes debugging less straightforward
- Performance Overhead: Chain traversal introduces additional processing steps compared to direct handlnig
Core Principle
The essence of the Chain of Responsibility pattern is responsibility separation with dynamic composition. Processing responsibilities are separated into distinct handler classes that can be composed flexibly at runtime, enabling adaptable request processing workflows.