Chain of Responsibility Pattern - 2

The Chain of Responsibility pattern is a behavioral design technique that lets you pass a request along a linked series of handlers until one of them decides to act on it. The sender of the request knows nothing about which object will finally process the request, nor does it need to understand the internal structure of the chain. This loose coupling makes the system easier to extend and modify.

  • Handler (abstract) – Declares the interface for handling a request and typically holds a reference to the next handler in the chain.
  • ConcreteHandler – Implements the handling logic. If it can satisfy the request, it does so; otherwise it forwards the request to its successor.
  • Client – Assembles the chain and initiates the request by invoking the first handler.
  • Request (optional) – An object that encapsulates all data needed for processing. Wrapping the parameters in a request object keeps the handler signatures clean and makes it easier to add new data later.

How the Chain Operates

  1. The client sends a request to the head of the chain.
  2. Each handler examines the request:
    • Can handle – Executes its business logic. Depending on the scenario, it may stop the propagation or let the request continue.
    • Cannot handle – Passes the request to the next handler via the stored reference.
  3. If the request reaches the tail without being handled, the system must decide how to react—throw an exception, return a default value, or log the failure.

Two variants exist:

  • Pure chain – A request is processed by at most one handler. Once a handler handles it, the chain stops.
  • Impure chain – After handling, a handler may still forward the request. Typical examples include servlet filters, Spring interceptors, or logging pipelines where multiple components act on the same request.

Illustrative Java Example

Below is a classic approval workflow. A Manager can approve expenses up to 1,000, a Director up to 5,000, and a CEO up to 10,000. Requests beyond that limit are rejected.

abstract class Approver {
    protected Approver next;
    public void setNext(Approver next) { this.next = next; }
    public abstract void approve(double amount);
}

class Manager extends Approver {
    @Override
    public void approve(double amount) {
        if (amount <= 1_000) {
            System.out.println("Manager approved: " + amount);
        } else if (next != null) {
            next.approve(amount);
        }
    }
}

class Director extends Approver {
    @Override
    public void approve(double amount) {
        if (amount <= 5_000) {
            System.out.println("Director approved: " + amount);
        } else if (next != null) {
            next.approve(amount);
        }
    }
}

class CEO extends Approver {
    @Override
    public void approve(double amount) {
        if (amount <= 10_000) {
            System.out.println("CEO approved: " + amount);
        } else {
            System.out.println("Amount exceeds approval limits: " + amount);
        }
    }
}

public class Client {
    public static void main(String[] args) {
        Approver manager = new Manager();
        Approver director = new Director();
        Approver ceo = new CEO();

        manager.setNext(director);
        director.setNext(ceo);

        manager.approve(800);    // Manager approves
        manager.approve(3_500);  // Director approves
        manager.approve(9_000);  // CEO approves
        manager.approve(20_000); // Rejected
    }
}

Output

Manager approved: 800.0
Director approved: 3500.0
CEO approved: 9000.0
Amount exceeds approval limits: 20000.0

Notice that the client interacts only with the abstract Approver. Adding a new role—say, VicePresident—requires only a new concrete class and an insertion point in the chain; existing classes stay untouched.

Implementation Tips

  • Inject the successor – Use constructor injection whenever possible; it guarantees that the next reference is set at creation time and prevents accidental null links.
  • Define tail behavior – Explicitly decide what happens when no handler can process the request (exception, default response, logging). This avoids silent failures.
  • Guard against cycles – When building the chain dynamically, verify that you are not creating a loop, which would cause infinite recursion.
  • Separate handling from forwarding – If a handler should continue the chain after processing, call next.handle(request) explicitly; otherwise, stop the propagation to keep the intent clear.
  • Watch chain depth – Very long chains increase call overhead. Consider grouping handlers, using a lookup table, or applying a skip‑list technique if performance becomes a concern.

Advantages and Drawbacks

Advantages

  • Decouples sender and receiver – The client only knows the first handler, not the whole processing network.
  • Dynamic composition – Handlers can be added, removed, or reordered at runtime, satisfying the Open/Closed Principle.
  • Single responsibility – Each handler focuses on a specific concern, simplifying testing and maintenance.

Drawbacks

  • No guarantee of handling – If the chain ends without a suitable handler, the request may be lost unless a fallback is provided.
  • Potential performance hit – Traversing many handlers can be costly, especially when most of them reject the request early.
  • Configuration complexity – When many handlers depend on each other implicitly, assembling the chain correctly can become intricate.

Typical Use Cases

  • Multi‑level approval processes (expense reports, leave requests, purchase orders).
  • Logging frameworks that filter messages by severity (DEBUG → INFO → WARN → ERROR).
  • Web request pipelines such as servlet filters, Spring MVC interceptors, or API gateway authentication chains.
  • Validation stacks for input data, security checks, or exception handling where each layer adds its own rule.

Closing Thoughts

The Chain of Responsibility pattern shifts the decision “who handles this request?” from compile time to runtime. By arranging handlers in a linear structure, you gain flexibility in how requests are processed and in what order. When applying the pattern, remember to define a clear termination strategy, protect the chain from accidental cycles, and choose between a pure or impure chain based on whether multiple handlers should act on the same request. With those safeguards in place, the pattern becomes a powerful tool for building extensible, loosely coupled systems.