Understanding the Chain of Responsibility Design Pattern

The Chain of Responsibility pattern is a behavioral design pattern that decouples the sender of a request from its receiver by giving multiple objects a chance to handle the request. These objects are linked into a chain, and the request travels along this chain until one of the handlers processes it. This approach allows you to add or modify handlers without affecting the client code.

Motivating Example: Purchase Approval System

Consider a Supply Chain Management (SCM) system where purchase requests must be approved based on their amount. The approval rules are as follows:

  • Director can approve requests under $5,000.
  • Vice President can approve requests under $10,000.
  • President can approve requests under $50,000.
  • Requests of $50,000 or more require board approval.

A naive implementation would place all logic in a single class, leading to code that violates the Single Responsibility and Open/Closed principles. Any change to approval limits or the addition of a new approver would require modifying and retesting the entire class. The Chain of Responsibility pattern offers a more flexible solution.

Pattern Structure

Chain of Responsibility UML Diagram

The pattern consists of the following key participants:

  • Handler (Abstract): Declares an interface for handling requests. It typically holds a reference to the next handler in the chain (the successor).
  • ConcreteHandler: Implements the request handling method. It either processes the request if it can, or forwards it to its successor.
  • Client: Initiates the request and configures the chain of handlers.

Implementation in Java

We can implement the purchase approval system using the Chain of Responsibility pattern. First, define the request class:

class PurchaseRequest {
    private double amount;
    private int number;
    private String purpose;

    public PurchaseRequest(double amount, int number, String purpose) {
        this.amount = amount;
        this.number = number;
        this.purpose = purpose;
    }

    public double getAmount() { return amount; }
    public int getNumber() { return number; }
    public String getPurpose() { return purpose; }
}

Next, define the abstract handler:

abstract class Approver {
    protected String name;
    protected Approver nextApprover;

    public Approver(String name) {
        this.name = name;
    }

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

    public abstract void processRequest(PurchaseRequest request);
}

Now, implement the concrete handlers:

class Director extends Approver {
    public Director(String name) { super(name); }

    @Override
    public void processRequest(PurchaseRequest request) {
        if (request.getAmount() < 5000) {
            System.out.println("Director " + name + " approved purchase #" + request.getNumber());
        } else if (nextApprover != null) {
            nextApprover.processRequest(request);
        }
    }
}

class VicePresident extends Approver {
    public VicePresident(String name) { super(name); }

    @Override
    public void processRequest(PurchaseRequest request) {
        if (request.getAmount() < 10000) {
            System.out.println("VP " + name + " approved purchase #" + request.getNumber());
        } else if (nextApprover != null) {
            nextApprover.processRequest(request);
        }
    }
}

class President extends Approver {
    public President(String name) { super(name); }

    @Override
    public void processRequest(PurchaseRequest request) {
        if (request.getAmount() < 50000) {
            System.out.println("President " + name + " approved purchase #" + request.getNumber());
        } else if (nextApprover != null) {
            nextApprover.processRequest(request);
        }
    }
}

class Congress extends Approver {
    public Congress(String name) { super(name); }

    @Override
    public void processRequest(PurchaseRequest request) {
        System.out.println("Board of Directors approved purchase #" + request.getNumber());
    }
}

Finally, the client configures the chain and sends requests:

public class Client {
    public static void main(String[] args) {
        Approver director = new Director("Alice");
        Approver vp = new VicePresident("Bob");
        Approver president = new President("Charlie");
        Approver board = new Congress("Board");

        director.setNextApprover(vp);
        vp.setNextApprover(president);
        president.setNextApprover(board);

        PurchaseRequest req1 = new PurchaseRequest(4000, 1001, "Office supplies");
        director.processRequest(req1);

        PurchaseRequest req2 = new PurchaseRequest(7000, 1002, "New laptops");
        director.processRequest(req2);

        PurchaseRequest req3 = new PurchaseRequest(25000, 1003, "Server upgrade");
        director.processRequest(req3);

        PurchaseRequest req4 = new PurchaseRequest(60000, 1004, "Building renovation");
        director.processRequest(req4);
    }
}

Expected Output:

Director Alice approved purchase #1001
VP Bob approved purchase #1002
President Charlie approved purchase #1003
Board of Directors approved purchase #1004

Adding a New Handler

If a new role, such as Manager (handles requests up to $8,000), is needed, simply create a new class and adjust the chain in the client:

class Manager extends Approver {
    public Manager(String name) { super(name); }

    @Override
    public void processRequest(PurchaseRequest request) {
        if (request.getAmount() < 8000) {
            System.out.println("Manager " + name + " approved purchase #" + request.getNumber());
        } else if (nextApprover != null) {
            nextApprover.processRequest(request);
        }
    }
}

// In Client.main():
Approver manager = new Manager("Diana");
director.setNextApprover(manager);
manager.setNextApprover(vp);
// rest of chain remains the same

This change requires no modification to existing handler classes, adhering to the Open/Closed Principle.

Pure vs. Impure Chain of Responsibility

  • Pure Chain: Each handler either fully processes the request or passes it entirely to the next handler. A request must be handled by exactly one handler.
  • Impure Chain: Handlers can process part of request and still pass it along. A request might not be handled at all. This is seen in event bubbling mechanisms in JavaScript.

Advantages and Disadvantages

Advantages:

  • Reduces coupling between sender and receiver.
  • Simplifies object connections by only requiring knowledge of the next handler.
  • Allows dynamic addition or reordering of handlers at runtime.
  • Facilitates adding new handlers without changing existing code.

Disadvantages:

  • No guarantee that a request will be handled; it may reach the end of the chain unprocessed.
  • Long chains can impact performance and complicate debugging.
  • Improper chain configuration can lead to circular references and infinite loops.

When to Use

  • Multiple objects can handle a request, and the appropriate handler is determined at runtime.
  • You want to decouple the request sender from multiple potential receivers.
  • The set of handlers can change dynamically.

Exercise

Design a leave approval system for an OA application using the Chain of Responsibility pattern:

  • Director approves leaves < 3 days.
  • Manager approves leaves < 10 days.
  • General Manager approves leaves < 30 days.
  • Leaves ≥ 30 days are rejected.

Tags: Chain of Responsibility Design Patterns java Behavioral Patterns

Posted on Tue, 23 Jun 2026 17:01:14 +0000 by marcs910