Skip to main content
Create your own
Lesson illustration

Chain of Responsibility: Decoupling Senders and Receivers

Hello! Welcome back to our module on behavioral design patterns.

In our last lesson, we explored the Observer pattern, which is excellent for broadcasting updates from one subject to many observers in a decoupled way. This creates a "one-to-many" communication channel.

Today, we'll look at a different kind of decoupling with the Chain of Responsibility pattern. Your learning outcome is to apply the Chain of Responsibility pattern to decouple senders and receivers of a request. Instead of a broadcast, this pattern creates a sequential chain of potential handlers for a request, allowing it to be passed along until it's processed. This is invaluable for creating flexible and extensible processing pipelines, a common task in system design.

The Problem: Rigid and Coupled Processing Logic

Imagine you're building a feature in a web application, perhaps an ordering system. Before an order request can be fully processed, it needs to go through a series of checks:

  1. Is the user authenticated?
  2. Does the request data pass validation?
  3. Are there any cached results we can use instead of hitting the database?

A straightforward approach would be to put all this logic into a single large method or class. But as more checks are added, this code becomes bloated and difficult to maintain. Changing the order of checks or reusing them elsewhere becomes a major refactoring effort.

To understand this problem more deeply, let's look at a well-described scenario.

Chain of Responsibility

The article 'Chain of Responsibility' from Refactoring.guru clearly outlines the problem of tightly coupled sequential checks in an online ordering system. This scenario is very relevant to the kind of backend systems you build with Spring Boot.

Please read the 'Problem' section. As you read, think about how you might have implemented these kinds of sequential checks in your own projects and the maintenance challenges they can create.

The core issue is that the client code that initiates the request is tightly coupled to the monolithic processing logic. We need a way to break this apart.

The Solution: A Chain of Independent Handlers

The Chain of Responsibility pattern solves this by turning each processing step into a standalone object called a handler. These handlers are then linked together to form a chain. A request enters the chain at the first handler, which decides to either:

  1. Process the request.
  2. Pass the request to the next handler in the chain.

This simple idea has powerful implications for decoupling and flexibility.

To get an intuitive feel for this, let's start with a short video that uses a great analogy.

The Chain of Responsibility Pattern Explained & Implemented | Behavioral Design Patterns | Geekific

This video from Geekific, 'The Chain of Responsibility Pattern Explained & Implemented', starts with a very relatable analogy of a customer service call being passed from one operator to the next.

Watch the first 1 minute and 33 seconds. This will give you a strong mental model of how a request flows through a chain of potential handlers.

The Structure of the Pattern

The pattern is composed of a few key roles. Let's formalize the structure.

  1. Handler Interface/Abstract Class: This defines the common interface for all concrete handlers. It typically includes a method to handle the request (e.g., handleRequest()) and a field to hold a reference to the next handler in the chain.
  2. Concrete Handlers: These are the individual processing objects. Each one implements the handler interface and contains the logic for a specific task. A handler inspects the request and decides whether it can process it. If it can't, or if processing should continue, it passes the request to the next handler.
  3. Client: The client is responsible for building the chain of handlers and initiating the request by passing it to the first handler in the chain.

This UML diagram shows a classic logging system example, which clearly illustrates the structure.

UML Class Diagram for Chain of Responsibility Pattern (Logger Example)
This diagram shows an `AbstractLogger` as the base handler, which holds a reference to the `nextLogger`. Concrete handlers like `ConsoleLogger` and `FileLogger` extend it, each capable of handling a log message or passing it down the chain.

It's important to note there are two common ways a chain can behave:

  • Sequential Processing: Every handler in the chain gets a chance to process the request (e.g., a series of validation checks).
  • Exclusive Handling: The first handler that can process the request does so, and the request is not passed any further down the chain (e.g., finding the right payment processor for a transaction).

Implementation in Java

Let's look at how this pattern is implemented in Java. We'll explore two examples that demonstrate the two behavioral modes we just discussed.

Example 1: Sequential Processing (Authentication)

This first example demonstrates a chain where a request must pass through multiple checks successfully.

The Chain of Responsibility Pattern Explained & Implemented | Behavioral Design Patterns | Geekific

Returning to the Geekific video, we'll now see a practical Java implementation for an authentication system. This is a perfect example of sequential processing where multiple checks (user exists, password is valid, role check) must all be passed.

Please watch from 01:33 to 06:37. Pay close attention to: The Handler abstract class with its next field. The implementation of the concrete handlers (UserExistsHandler, ValidPasswordHandler). How the client assembles the chain.

In this style, each handler performs its check and then explicitly calls the handle method on the next handler in the chain if the check passes.

Example 2: Exclusive Handling (Payment Processing)

Now, let's look at the other style, where the goal is to find the one correct handler for a request.

Chain of Responsibility Design Pattern in detail | Interview Question

This video from Daily Code Buffer, 'Chain of Responsibility Design Pattern in detail', uses a payment processing system as an example. This illustrates the 'exclusive handling' approach where the first handler that can process the payment does so, and the chain stops.

Watch from 04:22 to 09:11. Focus on: The scenario: different handlers for different payment amounts. The if/else logic within each handler: if I can handle it, I will; otherwise, I'll pass it to next. How the client builds the chain and sends requests with different amounts.

This if-else-next structure is characteristic of chains where handlers have distinct, non-overlapping responsibilities.

Test your understanding!

You are designing an expense approval system. The rules are:

  • Expenses up to $500 can be approved by a Manager.
  • Expenses between $501 and $5,000 can be approved by a Director.
  • Expenses over $5,000 must be approved by a VicePresident.

How would you model this using the Chain of Responsibility pattern? Identify the handlers and describe the logic inside one of them.

Show answer
  • Handlers: You would create three concrete handler classes: ManagerApprover, DirectorApprover, and VicePresidentApprover. They would all extend a common Approver abstract class or implement an Approver interface.

  • Chain: The client would build the chain in ascending order of authority: ManagerApprover -> DirectorApprover -> VicePresidentApprover.

  • Handler Logic (e.g., DirectorApprover):

    @Override
    public void approve(ExpenseReport report) {
        if (report.getAmount() > 500 && report.getAmount() <= 5000) {
            System.out.println("Expense approved by Director.");
        } else if (nextApprover != null) {
            nextApprover.approve(report);
        } else {
            System.out.println("No one in the chain can approve this expense.");
        }
    }
    

This is a classic example of the "exclusive handling" variant of the pattern.

Real-World Application: Servlet Filters

Given your background in Java Spring Boot, you've already used the Chain of Responsibility pattern, perhaps without realizing it. The javax.servlet.Filter mechanism is a textbook implementation.

Chain of Responsibility Design Pattern in Java

The Baeldung article 'Chain of Responsibility Design Pattern in Java' has an excellent section on this real-world use case. This will connect the abstract pattern directly to the technology you use daily.

Read section 5, 'Usage in the Real World'. Note how the doFilter method takes a FilterChain object. Calling chain.doFilter(request, response) is the explicit action of passing the request to the next handler in the chain.

Every time you configure a filter for tasks like security (SpringSecurityFilterChain), logging, or character encoding in a web application, you are adding a link to a Chain of Responsibility. Each filter is a handler that can inspect and modify the request/response, and then decide whether to pass control to the next filter in the chain.

When to Use It and Potential Downsides

The pattern is powerful but isn't a universal solution.

Use the Chain of Responsibility pattern when:

  • You want to decouple the sender of a request from its receivers.
  • Multiple objects could handle a request, and the specific handler is determined at runtime.
  • You need to execute a series of handlers in a specific order.
  • The set of handlers and their order might need to change dynamically.

Potential Downsides:

  • Request might go unhandled: If no handler in the chain processes the request, it simply falls off the end. This might be desired behavior, but it could also be an error that's hard to trace.
  • Debugging complexity: A request may pass through numerous handlers, making it more challenging to debug and trace its path compared to a single, monolithic method.

Conclusion

Today, we've broken down the Chain of Responsibility pattern, a key tool for creating clean, decoupled processing pipelines.

Key Takeaways:

  • The pattern passes a request along a chain of handlers, decoupling the sender from any specific receiver.
  • Each handler decides whether to process the request or pass it to the next handler.
  • It improves flexibility and adheres to the Single Responsibility and Open/Closed principles by isolating processing logic into separate classes.
  • A well-known real-world implementation in the Java ecosystem is the ServletFilter chain.

In our next lesson, we will discuss the Mediator pattern. While the Chain of Responsibility defines a clear, linear path for a request, the Mediator pattern centralizes complex communications between a set of objects. It prevents a "spaghetti" of connections by having all objects talk through a central hub instead of directly to each other.

Can't find a good explanation? Sign up and we'll make it for you

Sign up