Chain of Responsibility Pattern in Java – Decouple Senders and Receivers for Flexible Request Handling

Illustration for Chain of Responsibility Pattern in Java – Decouple Senders and Receivers for Flexible Request Handling
By Last updated:

Introduction

The Chain of Responsibility (CoR) Pattern is a behavioral design pattern that allows a request to be passed along a chain of handlers until one of them handles it. This pattern decouples the sender and receiver, making systems more flexible and modular.

Why Chain of Responsibility Matters

When dealing with request processing, validation, or event handling, a system should not have one hardcoded handler. Instead, it should allow dynamic, reusable, and ordered handlers—this is what CoR provides.


Core Intent and Participants

  • Intent: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.

Participants

  • Handler: Declares an interface for handling requests and linking to the next handler.
  • ConcreteHandler: Handles requests it is responsible for or forwards to the next handler.
  • Client: Sends requests into the chain.

UML Diagram (Text)

+--------+     +----------------+     +----------------+
| Client | --> | ConcreteHandler| --> | ConcreteHandler| --> ...
+--------+     +----------------+     +----------------+
                    ^
                    |
              +-------------+
              |   Handler   |
              +-------------+
              | + setNext() |
              | + handle()  |
              +-------------+

Real-World Use Cases

  • Servlet filters and middleware pipelines
  • Logging frameworks (log level handlers)
  • Request validation chains
  • Event bubbling in GUIs
  • Customer support ticket routing

Java Implementation Strategy

Example: Support Ticket System

Step 1: Handler Interface

public abstract class SupportHandler {
    protected SupportHandler next;

    public void setNext(SupportHandler next) {
        this.next = next;
    }

    public abstract void handleRequest(String issueType);
}

Step 2: Concrete Handlers

public class LevelOneSupport extends SupportHandler {
    public void handleRequest(String issueType) {
        if (issueType.equalsIgnoreCase("basic")) {
            System.out.println("Level 1 Support handled basic issue.");
        } else if (next != null) {
            next.handleRequest(issueType);
        }
    }
}

public class LevelTwoSupport extends SupportHandler {
    public void handleRequest(String issueType) {
        if (issueType.equalsIgnoreCase("intermediate")) {
            System.out.println("Level 2 Support handled intermediate issue.");
        } else if (next != null) {
            next.handleRequest(issueType);
        }
    }
}

public class LevelThreeSupport extends SupportHandler {
    public void handleRequest(String issueType) {
        if (issueType.equalsIgnoreCase("advanced")) {
            System.out.println("Level 3 Support handled advanced issue.");
        } else {
            System.out.println("No support level available for this issue.");
        }
    }
}

Step 3: Client Code

public class ChainDemo {
    public static void main(String[] args) {
        SupportHandler level1 = new LevelOneSupport();
        SupportHandler level2 = new LevelTwoSupport();
        SupportHandler level3 = new LevelThreeSupport();

        level1.setNext(level2);
        level2.setNext(level3);

        level1.handleRequest("basic");
        level1.handleRequest("intermediate");
        level1.handleRequest("advanced");
        level1.handleRequest("unknown");
    }
}

✅ Output shows how the request is passed along the chain.


Pros and Cons

✅ Pros

  • Reduces coupling between sender and receivers
  • Flexible order and addition of handlers
  • Promotes reusability of handler logic

❌ Cons

  • Can be hard to debug due to dynamic routing
  • Request may not be handled if no handler accepts it
  • Increased complexity with long chains

Anti-Patterns and Misuse

  • Overcomplicating simple linear logic with unnecessary chains
  • Catch-all handler that defeats the purpose of the pattern
  • Lack of proper fallback can lead to unhandled requests

Chain of Responsibility vs Strategy vs Observer

Pattern Purpose Key Concept Client Aware of Receiver?
Chain of Responsibility Decouple sender/receiver in order Request routing ❌ No
Strategy Select behavior dynamically Behavior switching ✅ Yes
Observer Notify all interested subscribers Event notification ❌ No

Refactoring Legacy Code

Before

if (type.equals("basic")) { ... }
else if (type.equals("intermediate")) { ... }
else if (type.equals("advanced")) { ... }

After (Using Chain of Responsibility)

level1.handleRequest(type);

✅ Cleaner, testable, and extensible code.


Best Practices

  • Keep handlers single-responsibility focused
  • Clearly define when a handler should process or pass the request
  • Avoid long unmaintainable chains—group or categorize them
  • Use enums or request objects instead of raw strings

Real-World Analogy

Think of customer support tiers. A basic issue is resolved by the front desk. If they can’t help, it goes to a technician, and then to engineering. Each level passes the request only if it cannot handle it—that’s the Chain of Responsibility.


Java Version Relevance

  • Java 8+: Use lambdas and functional handlers in dynamic chains
  • Java 17+: Use records to define immutable request objects
  • Spring: Filters and interceptors are built on this principle

Conclusion & Key Takeaways

  • Chain of Responsibility decouples sender and receivers for clean request handling.
  • Ideal for logging, validation, authorization, and dynamic event processing.
  • Make each handler focused and easily swappable.
  • Use it when requests must be processed conditionally across a pipeline.

FAQ – Chain of Responsibility Pattern in Java

1. What is the Chain of Responsibility Pattern?

A behavioral design pattern that allows passing a request along a chain of handlers.

2. What problems does it solve?

Decouples sender from receiver, promotes flexibility in request processing.

3. Where is it used in real life?

Logging, UI event bubbling, validation frameworks, servlet filters.

4. Is it the same as Strategy?

No. Strategy chooses behavior; CoR passes request across handlers.

5. What if no handler can process the request?

You can log a default message or throw an exception.

6. Can I break the chain early?

Yes. If a handler processes the request, it can stop propagation.

7. Is this pattern used in Spring?

Yes. Filters, interceptors, and handler mappings follow CoR.

8. Can this be combined with the Composite Pattern?

Yes. A tree-based command structure can use both.

9. How do I test handlers?

Mock the next handler and test each handler in isolation.

10. Is it good for REST APIs?

Yes, especially for authorization, validation, and request pre-processing.