Behavioral

Chain of Responsibility

Pass a request along a chain of handlers — each handler decides to process it or pass it to the next.

Intermediate Pattern #13 of 23

What is Chain of Responsibility?

Chain of Responsibility lets you pass a request through a chain of handler objects. Each handler decides either to process the request and stop the chain, or pass it to the next handler. The sender doesn't know which handler will ultimately process it.

This decouples the sender from all potential receivers — you can build, reorder, or extend the chain without changing any existing handler or the sender.

Real-world analogy: A customer support escalation chain — Level 1 support handles basic issues. If it can't resolve it, passes to Level 2. If still unresolved, escalates to Level 3. Each level either handles or passes on.

The Problem It Solves

You're building an expense approval system. Requests under $500 are approved by a Team Lead. $500–$5000 by a Manager. $5000–$50000 by a Director. Above that, only the CEO can approve. Hardcoding a long if-else chain couples the logic in one place and makes it painful to add new approval levels.

Chain of Responsibility lets each approver be a self-contained handler. Assembling or reordering the chain at runtime requires zero changes to handler code.

Participants

Handler (interface)
Declares the interface for handling requests. Usually contains a method to set the next handler and a method to handle the request.
BaseHandler (abstract)
Optional boilerplate class implementing the chain-linking logic. Stores next handler reference and calls it if the current handler passes the request.
ConcreteHandler
Handles requests it is responsible for. Calls next handler for everything else. Self-contained — knows only its own logic and the next handler.
Client
Assembles the chain and sends requests to the first handler. Unaware of which specific handler processes each request.

Visual Flow Diagram

Request sender TeamLead <$500? ✓ handle else → next Manager <$5000? ✓ handle else → next Director <$50000? ✓ handle else reject pass pass handled handled handled

Java Code Example

Java — Expense Approval Chain
// Handler interface
public interface Approver {
    void setNext(Approver next);
    void approve(double amount);
}

// Abstract base handler — boilerplate chain logic
public abstract class BaseApprover implements Approver {
    private Approver next;

    public Approver setNext(Approver next) {
        this.next = next;
        return next; // allows chaining: a.setNext(b).setNext(c)
    }

    protected void passToNext(double amount) {
        if (next != null) {
            next.approve(amount);
        } else {
            System.out.println("No handler available for $" + amount);
        }
    }
}

// Concrete Handlers
public class TeamLead extends BaseApprover {
    public void approve(double amount) {
        if (amount < 500) {
            System.out.println("TeamLead approved $" + amount);
        } else {
            passToNext(amount); // can't handle — pass up
        }
    }
}

public class Manager extends BaseApprover {
    public void approve(double amount) {
        if (amount < 5_000) {
            System.out.println("Manager approved $" + amount);
        } else {
            passToNext(amount);
        }
    }
}

public class Director extends BaseApprover {
    public void approve(double amount) {
        if (amount < 50_000) {
            System.out.println("Director approved $" + amount);
        } else {
            passToNext(amount);
        }
    }
}

// Client — assembles chain
public class Main {
    public static void main(String[] args) {
        BaseApprover lead    = new TeamLead();
        BaseApprover manager = new Manager();
        BaseApprover director= new Director();

        // Build chain: lead → manager → director
        lead.setNext(manager).setNext(director);

        lead.approve(200);     // TeamLead approved
        lead.approve(1500);    // Manager approved
        lead.approve(20000);   // Director approved
        lead.approve(100000);  // No handler available
    }
}

When to Use / Avoid

✓ Use When

  • More than one object may handle a request, and the handler isn't known a priori
  • You want to issue a request to one of several objects without specifying the receiver explicitly
  • The set of handlers should be specifiable dynamically
  • Middleware pipelines, filter chains, event propagation

✕ Avoid When

  • Every request must be guaranteed to be handled — broken chains silently drop requests
  • Performance is critical — long chains add latency
  • The handler logic is simple and static — an if-else or strategy is simpler

Real-World Examples

Pros & Cons

Pros

  • Decouples sender from receivers — sender knows nothing about handlers
  • Single Responsibility — each handler does one thing
  • Open/Closed — add new handlers without changing existing ones
  • Chain order is configurable at runtime

Cons

  • No guarantee a request gets handled — can fall off the end of the chain
  • Hard to debug — difficult to trace which handler processed a request
  • Long chains hurt performance and stack depth

How Chain of Responsibility Can Be Broken

⚠ Attack Vectors

  • Broken chain — request silently dropped: A handler neither processes the request nor calls passToNext() — the request disappears with no error
  • Circular chain: Handler A sets next to B, B sets next to A — infinite loop, StackOverflowError
  • Handler modifying shared request object: One handler mutates the request before passing it — downstream handlers see corrupted state
  • Assembling chain in wrong order: Chain is wired lead → director → manager instead of lead → manager → director — director approves small amounts that should go to manager
  • Stateful handlers shared across threads: Handler stores per-request state in instance fields — concurrent requests overwrite each other's state

✓ Prevention

  • Default passToNext in base class: The abstract base handler always calls next if defined — ConcreteHandlers only need to call super.handle() or passToNext() to stay in chain
  • Terminal handler: Always add a terminal catch-all handler at the end of the chain that logs or throws an exception if no handler could process the request
  • Immutable request objects: Make the request a value object with no setters — handlers can read it but never mutate it
  • Cycle detection: When building the chain with setNext(), check that the new next handler isn't already in the chain
  • Stateless handlers: Keep all handler classes stateless — all per-request data lives in the request object itself, not in handler fields
Java — Break & Fix
// ❌ BREAKING — handler swallows request silently
public void approve(double amount) {
    if (amount < 500) {
        System.out.println("Approved");
    }
    // ❌ else: nothing happens — request silently dropped
}

// ✅ FIX — always call passToNext in else branch
public void approve(double amount) {
    if (amount < 500) {
        System.out.println("TeamLead approved");
    } else {
        passToNext(amount); // ✅ always pass if not handled
    }
}

// ✅ Terminal handler — catches unhandled requests
public class UnhandledApprover extends BaseApprover {
    public void approve(double amount) {
        throw new IllegalStateException(
            "No approver for amount: $" + amount); // fail loudly
    }
}
// Wire it: lead.setNext(manager).setNext(director).setNext(new UnhandledApprover())

// ❌ BREAKING — handler mutates shared request
public void approve(Request req) {
    req.setAmount(req.getAmount() * 0.9); // ❌ mutates — next handler sees 10% less
    passToNext(req);
}

// ✅ FIX — immutable request object
public record Request(double amount, String description) {}
// records are immutable by default — no setters possible

How Other Patterns Relate

Interview Cheat Sheet

  1. What: A linked list of handlers. Request enters the first handler — it either processes it and stops, or passes to the next. Sender never knows which handler actually handles it.
  2. How: Handler interface with setNext() and handle(). Abstract BaseHandler stores next reference and calls it. ConcreteHandlers check condition — handle or call passToNext(). Client wires chain and sends to first.
  3. Critical interview point: Always add a terminal handler — silent request drops are the most common production bug with this pattern. Request objects should be immutable to prevent handler corruption.