The Pain of Rigid Approval Hierarchies

Picture this: You're building an expense management system for a growing company. Your initial approach seems logical: create a method that checks the expense amount and routes it to the appropriate approver based on predefined thresholds.

Then the organization evolves. The finance team wants to add department-specific approval rules. HR needs special handling for training expenses. The CEO wants certain categories to skip levels. Your approval method becomes a tangled mess of nested conditions that breaks every time the org chart changes.

Suddenly, you're dealing with code like this:

public ApprovalResult approveExpense(Expense expense) {
    if (expense.getAmount() <= 500) {
        return teamLead.approve(expense);
    } else if (expense.getAmount() <= 2000) {
        if (expense.getCategory().equals("TRAINING")) {
            return hrManager.approve(expense);
        }
        return manager.approve(expense);
    } else if (expense.getAmount() <= 10000) {
        if (expense.getDepartment().equals("ENGINEERING")) {
            return vpEngineering.approve(expense);
        }
        return director.approve(expense);
    }
    // ... endless conditions and special cases
}

Sound familiar? The Chain of Responsibility pattern solves this challenge by letting you pass requests along a chain of handlers, where each handler decides whether to process the request or pass it to the next handler in the chain.

Understanding the Chain of Responsibility Pattern

The Chain of Responsibility pattern decouples senders of requests from their receivers by giving multiple objects a chance to handle the request. The request passes along a chain until an object handles it, enabling dynamic request routing without tight coupling.

Think of it like an expense approval workflow in a company: when you submit an expense report, it doesn't go directly to the CEO. Instead, it starts with your team lead. If the amount exceeds their approval limit, they escalate it to the manager, who might escalate it to the director, and so on. Each person in the chain either approves the expense or passes it up the hierarchy.

This pattern promotes Loose Coupling, Single Responsibility, and Open Closed Principle while enabling flexible request handling at runtime.

Chain of Responsibilities Components

Core Components

  • Supporting Classes: Request and result objects that carry data through the chain (ExpenseRequest, ApprovalResult)

  • Abstract Handler: Defines the interface and implements common chaining logic for all handlers

  • Concrete Handlers: Extend the abstract handler with specific approval limits and processing logic

Complete Java Implementation

Let's build an expense approval system that demonstrates the Chain of Responsibility pattern's power in managing hierarchical approval workflows.

Supporting Classes

public class ExpenseRequest {
    private String employeeName;
    private String description;
    private double amount;
    private String category;

    public ExpenseRequest(String employeeName, String description,
                         double amount, String category) {
        this.employeeName = employeeName;
        this.description = description;
        this.amount = amount;
        this.category = category;
    }

    public String getEmployeeName() { return employeeName; }
    public String getDescription() { return description; }
    public double getAmount() { return amount; }
    public String getCategory() { return category; }

    @Override
    public String toString() {
        return String.format("%s - %s ($%.2f) [%s]",
                employeeName, description, amount, category);
    }
}

public class ApprovalResult {
    private boolean approved;
    private String approverRole;
    private String message;

    public ApprovalResult(boolean approved, String approverRole, String message) {
        this.approved = approved;
        this.approverRole = approverRole;
        this.message = message;
    }

    public boolean isApproved() { return approved; }
    public String getApproverRole() { return approverRole; }
    public String getMessage() { return message; }

    @Override
    public String toString() {
        String status = approved ? "Approved" : "Rejected";
        return String.format("%s by %s - %s", status, approverRole, message);
    }
}

Abstract Handler

public abstract class ExpenseApprover {
    protected ExpenseApprover nextApprover;
    protected double approvalLimit;
    protected String role;

    public ExpenseApprover(String role, double approvalLimit) {
        this.role = role;
        this.approvalLimit = approvalLimit;
    }

    public void setNextApprover(ExpenseApprover nextApprover) {
        this.nextApprover = nextApprover;
    }

    public String getApproverRole() {
        return role;
    }

    public ApprovalResult approve(ExpenseRequest request) {
        if (canApprove(request)) {
            return processApproval(request);
        } else if (nextApprover != null) {
            System.out.println(role + " escalating to " + nextApprover.getApproverRole());
            return nextApprover.approve(request);
        } else {
            return new ApprovalResult(false, role, "Exceeds maximum approval authority");
        }
    }

    protected boolean canApprove(ExpenseRequest request) {
        return request.getAmount() <= approvalLimit;
    }

    protected abstract ApprovalResult processApproval(ExpenseRequest request);
}

Concrete Handlers

public class TeamLead extends ExpenseApprover {

    public TeamLead() {
        super("Team Lead", 1000.00);
    }

    @Override
    protected ApprovalResult processApproval(ExpenseRequest request) {
        return new ApprovalResult(true, role, "Routine team expense");
    }
}

public class Manager extends ExpenseApprover {

    public Manager() {
        super("Manager", 5000.00);
    }

    @Override
    protected ApprovalResult processApproval(ExpenseRequest request) {
        return new ApprovalResult(true, role, "Departmental budget");
    }
}

public class Director extends ExpenseApprover {

    public Director() {
        super("Director", Double.MAX_VALUE);
    }

    @Override
    protected ApprovalResult processApproval(ExpenseRequest request) {
        return new ApprovalResult(true, role, "Strategic initiative");
    }
}

Client

public class ExpenseApprovalDemo {
    public static void main(String[] args) {
        System.out.println("=== Expense Approval System ===\n");

        // Build the chain of responsibility
        ExpenseApprover teamLead = new TeamLead();
        ExpenseApprover manager = new Manager();
        ExpenseApprover director = new Director();

        teamLead.setNextApprover(manager);
        manager.setNextApprover(director);

        // Process various expense requests
        ExpenseRequest[] requests = {
            new ExpenseRequest("Alice", "Office supplies", 750.00, "SUPPLIES"),
            new ExpenseRequest("Bob", "Team building event", 2500.00, "ENTERTAINMENT"),
            new ExpenseRequest("Carol", "Conference registration", 4800.00, "TRAINING"),
            new ExpenseRequest("David", "Server infrastructure", 15000.00, "EQUIPMENT")
        };

        for (ExpenseRequest request : requests) {
            System.out.println("Request: " + request);
            ApprovalResult result = teamLead.approve(request);
            System.out.println("Result: " + result);
            System.out.println();
        }
    }
}

Expected Output:

=== Expense Approval System ===

Request: Alice - Office supplies ($750.00) [SUPPLIES]
Result: Approved by Team Lead - Routine team expense

Request: Bob - Team building event ($2500.00) [ENTERTAINMENT]
Team Lead escalating to Manager
Result: Approved by Manager - Departmental budget

Request: Carol - Conference registration ($4800.00) [TRAINING]
Team Lead escalating to Manager
Result: Approved by Manager - Departmental budget

Request: David - Server infrastructure ($15000.00) [EQUIPMENT]
Team Lead escalating to Manager
Manager escalating to Director
Result: Approved by Director - Strategic initiative

🚀 Get the Complete Implementation

The full code with advanced chain configurations and dynamic handler insertion is available in our Design Patterns Repository.

# Clone and run the complete demo
git clone https://github.com/shift-elevate/design-patterns.git
cd design-patterns
mvn test -Dtest=ChainOfResponsibilityPatternTest

Real World Examples

The Chain of Responsibility pattern is extensively used in real-world applications:

1. Servlet Filters and Middleware

Web frameworks use filter chains to process HTTP requests through multiple handlers. Each filter can authenticate, log, compress, or modify the request before passing it to the next filter. This enables modular request processing where new filters can be added without modifying existing ones.

2. Event Handling in UI Frameworks

GUI frameworks use event bubbling where UI events propagate through a hierarchy of components. A button click event first goes to the button, then its container, then the window, until some handler processes it. This allows flexible event handling at any level of the component tree.

3. Logging Frameworks

Logging systems route messages through handler chains based on severity levels. A DEBUG message might go to a file handler, while an ERROR message triggers file logging, email notification, and alerting systems. Each handler decides whether to process the message based on its configured threshold.

When to Use Chain of Responsibility Pattern

Understanding when to apply the Chain of Responsibility pattern is crucial for making the right architectural decisions. Here's when it shines and when alternatives might be better:

Ideal Scenarios:

  • Multiple objects may handle a request, and the handler isn't known in advance.

  • You want to issue a request to several objects without specifying the receiver explicitly.

  • The set of handlers should be specified dynamically at runtime.

  • You want to decouple request senders from receivers.

  • Requests need to be processed by multiple handlers in sequence.

Skip It When:

  • There's always exactly one handler for each request type.

  • The handling logic is simple and unlikely to change.

  • You need guaranteed request handling (chains can leave requests unhandled).

  • Performance is critical and chain traversal overhead is unacceptable.

Next Steps: Apply Chain of Responsibility Pattern in Your Project

Ready to implement the Chain of Responsibility pattern in your own projects? Here's a structured approach to get you started:

  1. Identify Request Flow: Look for scenarios where requests pass through multiple processing stages (approvals, validations, transformations).

  2. Define Abstract Handler: Create an abstract class with common chaining logic and successor management.

  3. Create Concrete Handlers: Extend the abstract handler for each processing step with specific logic.

  4. Configure the Chain: Set up the chain order based on your business requirements.

  5. Add Fallback Handling: Ensure requests that reach the end of the chain are handled appropriately.

The Chain of Responsibility pattern transforms rigid, tightly-coupled request handling into flexible, extensible processing pipelines. By decoupling senders from receivers and enabling dynamic handler configuration, you build systems that adapt to changing business rules while maintaining clean, testable code.

Found this helpful? Share it with a colleague who's struggling with complex approval workflows or request routing logic. Got questions? We'd love to hear from you at [email protected]

Keep Reading

No posts found