Showing posts with label Core Java. Show all posts
Showing posts with label Core Java. Show all posts

Chain of Responsibility Design Pattern in Java: Complete Guide with Examples (2026)

Chain of Responsibility Design Pattern in Java: Complete Guide with Examples (2026)

Chain of Responsibility Design Pattern in Java: A Complete Guide with Real-World Examples

Learn the Chain of Responsibility design pattern in Java with simple explanations and practical examples. This guide covers how a request passes through multiple handlers, with before-and-after code for beginners and experts.

When you start working on a Java project, writing a few if-else conditions to handle business logic feels perfectly fine. The problem usually starts when the requirements keep growing. I have seen this kind of problem in applications where different business rules gradually get mixed into the same method. One practical way to organize such code is the Chain of Responsibility design pattern.

Why the Chain of Responsibility Pattern Still Matters in Java

Think about a payment application. Before processing a transaction, you may need to check whether the request is valid, whether the account is active, whether the transaction amount is within the allowed limit, and whether the payment looks suspicious.

Initially, you might put all these checks inside one service method. A few months later, that method may contain so many conditions that even a small change becomes difficult to manage. The Chain of Responsibility pattern solves this by giving each check its own class.

A request enters the chain, and each handler decides whether to process it, forward it, or stop the chain when the request cannot continue. Whether you're building payment pipelines, authentication flows, approval workflows, or request-processing middleware, Chain of Responsibility keeps your code flexible and your responsibilities separated.

What Is the Chain of Responsibility Design Pattern?

"Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it."

The Chain of Responsibility is a behavioral design pattern in which a request passes through a sequence of handlers. Each handler has a particular responsibility. It can process the request, pass it to the next handler, or stop the chain when the request cannot continue.

Let's take a simple example. Suppose you raise a technical issue with your company's support team. The first person checks whether they can resolve it. If they cannot, the issue moves to the next person. The process continues until someone handles it or the team determines that no available handler can resolve it.

Java applications can follow the same approach. Instead of putting every business rule into one large method, we create separate classes for individual tasks and connect them in the required order.

The Three Players

Role Responsibility Example
Handler Defines the common operation for processing and forwarding PaymentHandler
Concrete Handler Implements one specific check and decides to continue or reject TransactionLimitHandler
Client Builds the chain and submits the request to the first handler PaymentApplication
Chain of Responsibility pattern in a Java payment application A payment request passes through request validation, account verification, transaction limit checking, and a risk check before payment processing. A failed validation stops the chain. Payment Request Flow Payment Request Request Validation Account Verification Transaction Limit Check Risk Check Payment Processing All checks passed Request Rejected A failed check stops further processing Each handler owns one specific responsibility.

Chain of Responsibility example: each payment-validation handler checks the request before allowing it to continue.

How to read this diagram (for freshers): The blue path shows the happy flow — a Payment Request enters the chain and passes through Request Validation, Account Verification, Transaction Limit Check, and Risk Check. If every check passes, the green path leads to Payment Processing. If any mandatory check fails, the red path shows the request being rejected immediately — it never reaches the remaining handlers.

Understanding the Pattern Through a Payment Example

Before we touch code, let's look at the Chain of Responsibility pattern you already use every day.

Example 1: A Payment Gateway

Suppose you are developing a payment service using Java and Spring Boot. Before accepting a payment, the application needs to perform four checks:

  • Check whether the payment request contains valid data.
  • Verify that the customer's account is active.
  • Check whether the transaction amount exceeds the permitted limit.
  • Perform a basic risk check before allowing the payment to continue.

Each check serves a different purpose, so it makes sense to keep them separate. If the account is inactive, there is no reason to run the remaining checks. Similarly, if the transaction amount exceeds the permitted limit, the request should stop there. This is where Chain of Responsibility fits naturally.

Example 2: Spring Security Filter Chain (You Already Use This)

When an HTTP request hits a Spring Boot application secured with Spring Security, it passes through a filter chain. Each filter — authentication, CSRF protection, authorization — does its job and either continues the chain or stops the request.

That's Chain of Responsibility in the standard Spring ecosystem. The servlet filter chain is one of the most widely used implementations of this pattern in Java.

Example 3: Customer Support Escalation

You call customer support. The IVR (front-line handler) tries to resolve your issue. If it can't, it forwards you to a human agent. If the agent can't help, they escalate to a specialist. You, the caller, just initiate the request — the chain figures out who handles it.

Quick Summary for Freshers

Real-World Situation Handlers Stop Condition
Payment gateway Validation → Account → Limit → Risk Any check fails
Spring Security Authentication → CSRF → Authorization Request rejected or unauthorized
Customer support IVR → Agent → Specialist Issue resolved
Expense approval Manager → Finance → Director Approved or rejected at any stage

Once you see it in a payment pipeline, you'll see it everywhere. That's why Chain of Responsibility is one of the most practical behavioral design patterns when you have multiple independent checks.

Violation: Why Not Put Everything in One Service Method?

Let's look at a simple implementation without the pattern.

public void processPayment(PaymentRequest request) {

    if (request == null) {
        throw new IllegalArgumentException("Invalid request");
    }

    if (!accountService.isActive(request.getAccountId())) {
        throw new IllegalStateException("Account is inactive");
    }

    if (request.getAmount() > 50000) {
        throw new IllegalArgumentException("Transaction limit exceeded");
    }

    if (fraudService.isSuspicious(request)) {
        throw new IllegalStateException("Transaction rejected");
    }

    paymentService.execute(request);
}

The Problem: There is nothing inherently wrong with this code when the requirements are small. The problem appears when more checks are added and the method becomes responsible for too many things.

Imagine adding merchant verification, KYC checks, device validation, transaction velocity rules, and additional fraud checks. The service gradually becomes harder to maintain, and testing each rule independently becomes less convenient. This violates the Open/Closed principle — the method is closed to extension because every new rule forces a modification.

With Chain of Responsibility, each validation can live in its own handler. The main flow then focuses on connecting the handlers and starting the request.

Solution: Implementing Chain of Responsibility in Java

Before building the payment example, let's start with something smaller. This will make the main idea easier to understand.

Step 1: Create a Handler Interface

public interface SupportHandler {

    void setNext(SupportHandler next);

    void handle(String issue);
}

Every handler follows the same contract. It can receive an issue and, when necessary, forward it to another handler.

Step 2: Create a Base Handler

public abstract class BaseSupportHandler
        implements SupportHandler {

    protected SupportHandler next;

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

    protected void forward(String issue) {
        if (next != null) {
            next.handle(issue);
        } else {
            System.out.println(
                "No handler found for: " + issue
            );
        }
    }
}

The base class contains the common forwarding logic, so individual handlers do not need to repeat it.

Step 3: Create Concrete Handlers

public class TechnicalSupportHandler
        extends BaseSupportHandler {

    @Override
    public void handle(String issue) {

        if ("technical".equalsIgnoreCase(issue)) {
            System.out.println(
                "Technical team is handling the issue."
            );
        } else {
            forward(issue);
        }
    }
}
public class BillingSupportHandler
        extends BaseSupportHandler {

    @Override
    public void handle(String issue) {

        if ("billing".equalsIgnoreCase(issue)) {
            System.out.println(
                "Billing team is handling the issue."
            );
        } else {
            forward(issue);
        }
    }
}

Step 4: Connect the Handlers

public class ChainDemo {

    public static void main(String[] args) {

        SupportHandler technical =
                new TechnicalSupportHandler();

        SupportHandler billing =
                new BillingSupportHandler();

        technical.setNext(billing);

        technical.handle("technical");
        technical.handle("billing");
        technical.handle("general");
    }
}

Output:

Technical team is handling the issue.
Billing team is handling the issue.
No handler found for: general

Notice that the client starts processing from the first handler. It does not need to decide which handler should receive each request. The handlers themselves determine whether to process or forward it.

For Beginners: The Airport Checkpoint Analogy

Think of an airport. You pass through check-in, then security screening, then passport control, then the boarding gate. Each checkpoint has one job. If you fail security screening, you don't proceed to passport control — the chain stops. Each checkpoint doesn't need to know how the others work; it only needs to do its own job and pass you to the next checkpoint.

Chain of Responsibility is the airport checkpoints. Your request is the passenger.

A Practical Payment Validation Example

Now let's apply the same idea to a payment application. We will create a chain with request validation, account verification, transaction-limit checking, and fraud checking.

For this example, the handlers stop processing by throwing an exception when a mandatory check fails. In a larger application, you could return a validation result instead, especially when different rejection reasons need to be handled consistently.

1. Create the PaymentRequest Class

public class PaymentRequest {

    private final String accountId;
    private final double amount;

    public PaymentRequest(String accountId, double amount) {
        this.accountId = accountId;
        this.amount = amount;
    }

    public String getAccountId() {
        return accountId;
    }

    public double getAmount() {
        return amount;
    }
}

The request contains the account identifier and payment amount. For a production financial application, use BigDecimal or integer minor units rather than double to represent money.

2. Create the PaymentHandler Interface

public interface PaymentHandler {

    void setNext(PaymentHandler next);

    void handle(PaymentRequest request);
}

3. Create the Abstract Payment Handler

public abstract class AbstractPaymentHandler
        implements PaymentHandler {

    private PaymentHandler next;

    @Override
    public void setNext(PaymentHandler next) {
        this.next = next;
    }

    protected void forward(PaymentRequest request) {
        if (next != null) {
            next.handle(request);
        }
    }
}

This class keeps the next-handler reference and provides a common method for continuing the chain.

4. Validate the Request

public class RequestValidationHandler
        extends AbstractPaymentHandler {

    @Override
    public void handle(PaymentRequest request) {

        if (request == null
                || request.getAccountId() == null
                || request.getAccountId().isBlank()
                || !Double.isFinite(request.getAmount())
                || request.getAmount() <= 0) {

            throw new IllegalArgumentException(
                "Invalid payment request"
            );
        }

        System.out.println("Request validation passed.");
        forward(request);
    }
}

This handler rejects a missing request, an empty account identifier, or an invalid amount. The next handler runs only when the request passes these checks.

5. Verify the Account

public class AccountVerificationHandler
        extends AbstractPaymentHandler {

    @Override
    public void handle(PaymentRequest request) {

        // Demo rule. Replace with an account service
        // or repository lookup in a real application.
        boolean active =
                "ACC100".equals(request.getAccountId());

        if (!active) {
            throw new IllegalStateException(
                "Account is inactive or not found"
            );
        }

        System.out.println("Account verification passed.");
        forward(request);
    }
}

In a real application, account status should come from a trusted account service or database. The example uses a fixed account identifier simply to demonstrate the pattern.

6. Check the Transaction Limit

public class TransactionLimitHandler
        extends AbstractPaymentHandler {

    private static final double MAX_AMOUNT = 50000.00;

    @Override
    public void handle(PaymentRequest request) {

        if (request.getAmount() > MAX_AMOUNT) {
            throw new IllegalArgumentException(
                "Transaction limit exceeded"
            );
        }

        System.out.println("Transaction limit check passed.");
        forward(request);
    }
}

The handler checks the transaction against a simple maximum amount. Real transaction limits may depend on account type, customer risk, merchant rules, and other business requirements.

7. Perform a Basic Risk Check

public class FraudCheckHandler
        extends AbstractPaymentHandler {

    @Override
    public void handle(PaymentRequest request) {

        // Simplified demonstration rule only.
        boolean suspicious = request.getAmount() > 40000;

        if (suspicious) {
            throw new IllegalStateException(
                "Transaction requires risk review"
            );
        }

        System.out.println("Fraud check passed.");
        forward(request);
    }
}

This is not a real fraud detection algorithm. It simply demonstrates how an additional handler can apply a separate business rule without changing the other handlers.

8. Connect the Chain and Run the Application

public class PaymentApplication {

    public static void main(String[] args) {

        PaymentHandler validation =
                new RequestValidationHandler();

        PaymentHandler account =
                new AccountVerificationHandler();

        PaymentHandler limit =
                new TransactionLimitHandler();

        PaymentHandler fraud =
                new FraudCheckHandler();

        validation.setNext(account);
        account.setNext(limit);
        limit.setNext(fraud);

        PaymentRequest request =
                new PaymentRequest("ACC100", 10000.00);

        try {
            validation.handle(request);

            // Actual payment execution belongs here,
            // after all mandatory checks have passed.
            System.out.println(
                "Payment is ready for processing."
            );

        } catch (IllegalArgumentException
                 | IllegalStateException exception) {

            System.out.println(
                "Payment rejected: " + exception.getMessage()
            );
        }
    }
}

Expected output:

Request validation passed.
Account verification passed.
Transaction limit check passed.
Fraud check passed.
Payment is ready for processing.

If you change the amount to 60000.00, the transaction-limit handler throws an exception. The fraud handler does not run, and the application does not reach the payment-execution statement.

Important: Passing the validation chain does not automatically make a payment safe to execute. A production application still needs appropriate authorization, idempotency, concurrency control, and payment-provider error handling.

For Experts: Chain of Responsibility vs. Strategy (Know the Difference)

This is a common interview trap.

Aspect Chain of Responsibility Strategy
Purpose Pass a request through handlers Choose an algorithm or behavior
Execution Multiple handlers may run Usually one selected strategy runs
Example Payment validation Payment provider selection
Control Handlers can forward or stop the request The client or context selects a strategy

Rule of thumb: Chain of Responsibility passes a request through multiple handlers. Strategy selects one algorithm. For example, you could use Chain of Responsibility to validate a payment request and Strategy to choose which payment provider should process it. Both patterns can work together without replacing one another.

Using the Same Idea in Spring Boot

In Spring Boot, you do not always need to connect handlers manually. When several classes implement the same interface, Spring can inject them as a list.

This is convenient when the validation steps are configured as Spring beans and the order is important.

Create a Validation Result

public record ValidationResult(
        boolean valid,
        String message) {

    public static ValidationResult success() {
        return new ValidationResult(true, "OK");
    }

    public static ValidationResult failure(String message) {
        return new ValidationResult(false, message);
    }
}

Define the Validator Interface

public interface PaymentValidator {

    ValidationResult validate(PaymentRequest request);
}

Implement an Amount Validator

import org.springframework.stereotype.Component;

@Component
public class AmountValidator implements PaymentValidator {

    @Override
    public ValidationResult validate(PaymentRequest request) {

        if (request == null
                || !Double.isFinite(request.getAmount())
                || request.getAmount() <= 0) {

            return ValidationResult.failure(
                "Amount must be a positive, finite value"
            );
        }

        return ValidationResult.success();
    }
}

Create the Validation Pipeline

import java.util.List;
import org.springframework.stereotype.Service;

@Service
public class PaymentValidationChain {

    private final List<PaymentValidator> validators;

    public PaymentValidationChain(
            List<PaymentValidator> validators) {
        this.validators = validators;
    }

    public ValidationResult validate(PaymentRequest request) {

        for (PaymentValidator validator : validators) {

            ValidationResult result =
                    validator.validate(request);

            if (!result.valid()) {
                return result;
            }
        }

        return ValidationResult.success();
    }
}

Spring automatically injects the registered validator beans. If the order matters, annotate the implementations with @Order and use an explicit ordering convention.

This list-based approach is often convenient for Spring Boot validation pipelines. Strictly speaking, it is a pipeline-style implementation of the same idea rather than the linked-handler implementation shown earlier. Choose the version that best matches your application's requirements.

Chain of Responsibility vs. Strategy Pattern

These two patterns can appear similar because both use interfaces and separate implementations. The difference becomes clear when you look at how they are used.

Point Chain of Responsibility Strategy
Purpose Pass a request through handlers Choose an algorithm or behavior
Execution Multiple handlers may run Usually one selected strategy runs
Example Payment validation Payment provider selection
Control Handlers can forward or stop the request The client or context selects a strategy

For example, you could use Chain of Responsibility to validate a payment request and Strategy to choose which payment provider should process it. Both patterns can work together without replacing one another.

Advantages and Disadvantages

Advantages Disadvantages
  • Easier maintenance: each handler contains a smaller piece of business logic
  • Independent testing: test one validation rule without executing the entire workflow
  • Flexible configuration: handlers can be added or reordered when requirements change
  • Reduced coupling: the client does not need to know every handler's implementation
  • Clear responsibilities: developers can identify which class owns a particular rule
  • The request may reach the end of the chain without being handled
  • Debugging can take longer when many handlers are involved
  • Incorrect ordering can cause unnecessary processing or incorrect results
  • For a very small workflow, multiple handler classes may add needless complexity
  • Failure handling and the definition of a successful request must be clear

Frequently Asked Interview Questions

1. What is Chain of Responsibility in Java?

It is a behavioral design pattern in which a request moves through a sequence of handlers. Each handler can process the request, forward it, or stop further processing.

2. Can multiple handlers process the same request?

Yes. In a validation pipeline, multiple handlers can inspect the same request. In other implementations, one handler may take responsibility and prevent subsequent handlers from running.

3. What happens if no handler accepts a request?

The result depends on the implementation. The application can return an unhandled result, invoke a fallback handler, or raise an error. The behavior should be explicitly defined.

4. Is Chain of Responsibility a behavioral design pattern?

Yes. It focuses on how requests and responsibilities are distributed among collaborating objects.

5. What is the main advantage of this pattern?

It reduces coupling between the client and individual handlers and makes it easier to add, remove, or reorder processing steps. Each handler owns a single responsibility and can be tested independently.

6. Is Spring Security an example of Chain of Responsibility?

Spring Security's servlet security filter chain is a practical example of sequential request processing through multiple filters. Each filter can perform security-related work and continue the filter chain when appropriate.

7. How is Chain of Responsibility different from Strategy?

Chain of Responsibility distributes a request across a sequence of handlers, while Strategy allows a client to select a particular algorithm or behavior.

8. Can this pattern be used in microservices?

Yes. It can be used inside a microservice for validation, authorization, routing decisions, and request processing. Communication between separate microservices, however, introduces network and distributed-systems concerns that the pattern alone does not solve.

9. Does Chain of Responsibility follow SOLID principles?

A well-designed implementation can support the Single Responsibility Principle and Open/Closed Principle. Whether it follows SOLID depends on how responsibilities, dependencies, and extension points are designed.

10. When should you avoid this pattern?

Avoid it when there is only one simple operation, when a normal conditional is easier to understand, or when the added handler structure introduces more complexity than value.

Your Chain of Responsibility Checklist

Before refactoring your if-else ladder into a chain, ask:

  • Do I have multiple independent checks? If each rule is genuinely separate, a chain helps.
  • Will the rules grow or change? If new checks arrive frequently, a chain is easier to extend.
  • Does order matter? If yes, document and test the chain order explicitly.
  • What happens when no handler processes the request? Define the fallback behavior clearly.
  • Is the complexity worth it? For two static conditionals, a simple if-else is better.

Start small. Refactor one growing method. The maintainability gains compound.

Best Practices for Java Developers

  • Keep each handler focused on one responsibility.
  • Make the handler order explicit whenever business rules depend on it.
  • Decide clearly whether a handler forwards, rejects, or terminates a request.
  • Avoid putting every business rule into a single handler.
  • Write unit tests for both successful and failed validation scenarios.
  • Keep Spring singleton handlers stateless where possible.
  • Handle external-service timeouts and failures separately from business rejections.
  • Use meaningful validation results or domain exceptions instead of unclear generic errors.

Conclusion

The Chain of Responsibility pattern isn't about complexity — it's about separated responsibilities. When a request must pass through multiple independent checks, Chain of Responsibility gives each check its own class and lets you add, remove, or reorder them without touching the client. It trades a small upfront design decision for long-term flexibility.

I'll be honest: I don't use Chain of Responsibility in every project. Small apps with two static conditionals don't need it. But for anything that touches payments, authentication, approval workflows, or request-processing pipelines — where rules grow and change — it has saved me from rewriting the same large method over and over. Start with the problem, not the pattern. If you find yourself adding a new if to a method every sprint, Chain of Responsibility is probably the answer.

The most important lesson is simple: use the pattern when a request genuinely needs to pass through multiple independent responsibilities, not merely to replace every conditional statement with another class.

Related topics: Java Design Patterns with Real-World Examples, Strategy Design Pattern in Java, Factory Design Pattern in Java, Spring Boot Filters and Interceptors, SOLID Principles with Spring Boot Examples, Java Exception Handling Best Practices