Chain of Responsibility Design Pattern in Java

Learn Chain of Responsibility in Java with ordered discount handlers, working code, diagrams, short-circuit behavior, and explicit no-match handling.

A sender calls the first handler; each handler returns a result or passes the request to its successor.

The Chain of Responsibility pattern passes a request through a sequence of handlers, giving each handler an opportunity to handle it. The sender calls the first handler without selecting the eventual receiver. Each handler decides whether to produce a result or pass the request to its successor, which is the next handler in the chain.

In a first-match chain, processing stops when a handler accepts the request. If no handler accepts it, the chain returns an explicit unhandled result. The order and stopping rule are part of the application’s behavior, not incidental details of how the handlers are linked.

1. How the Chain of Responsibility Pattern Works

Handlers expose a common operation so each handler can forward a request without knowing the concrete class of its successor. Application setup connects the handlers and gives the sender a reference to the first one.

When a handler cannot handle a request, it calls the same operation on its successor and returns that result. When it can handle the request, it returns its own result immediately. The remaining handlers are not invoked.

The sender invokes Handler1. A handler returns a result when it accepts the request, or forwards it to the next handler. The last handler returns no match if it declines.

The pattern has three main roles:

Participant Responsibility
Sender Submits a request to the start of the chain.
Handler Defines the common operation and the forwarding behavior.
Concrete handler Decides whether it can handle the request and produces a result when it can.

A successor is another handler, not a separate kind of participant. The final handler has no successor, so the chain needs a defined result for a request that reaches the end without being handled.

A first-match chain stops on success; a validation chain commonly continues on success and stops on failure. Both arrangements pass work between handlers, but their stopping rules differ. The implementation below uses first-match behavior.

2. Chain of Responsibility Pattern in Action

An online store calculates a discount for a checkout. Its policy applies the first eligible discount and does not combine discounts. A recognized coupon takes priority over a loyalty discount, and a customer who qualifies for neither receives no discount.

For this example, the coupon SAVE10 gives a 10% discount and membership gives a 5% discount. These are illustrative rules for a single subtotal. The example calculates a discount amount; it does not charge a payment or redeem a coupon in a database.

Order Handler Eligibility Result
1 CouponHandler Coupon code equals SAVE10. 10% of the subtotal.
2 LoyaltyHandler Customer is a member. 5% of the subtotal.
End No handler accepted Neither rule applies. No discount.

DiscountRequest carries the subtotal, coupon code and membership flag. DiscountHandler defines the common operation, and the two concrete handlers implement the eligibility rules. ChainDemo constructs the chain and acts as the sender.

The first call looks like this. Its containing class imports java.math.BigDecimal and java.util.Optional:

DiscountHandler chain = new CouponHandler(new LoyaltyHandler(null));
DiscountRequest request = new DiscountRequest(
        new BigDecimal("100.00"), "SAVE10", true);
Optional<Discount> result = chain.handle(request);

Optional<Discount> represents either one discount or no eligible discount. An empty result means no rule matched; it is different from a selected discount whose rounded amount is zero. Here, the coupon handler returns a discount of 10.00. The loyalty handler is not called, even though the request also qualifies for that rule. The null constructor argument marks the end of the chain.

3. Implementing the Discount Chain

The complete source uses JDK 17 or later. Each public type is saved in its own file. Package declarations are included in the repository and omitted from the excerpts below.

3.1. Representing the Request and Result

A Java record is a concise declaration for a data carrier. It supplies accessors named after its components, so request.subtotal() returns the subtotal and request.member() returns the membership flag.

Our request validates its required values during construction:

import java.math.BigDecimal;
import java.util.Objects;

public record DiscountRequest(BigDecimal subtotal, String couponCode, boolean member) {
    public DiscountRequest {
        Objects.requireNonNull(subtotal, "subtotal is required");
        Objects.requireNonNull(couponCode, "couponCode is required");
        if (subtotal.signum() <= 0) {
            throw new IllegalArgumentException("subtotal must be positive");
        }
    }
}

The constructor uses the compact form supported by records: its parameters come from the record header, so they are not repeated after the constructor name. Validation runs before the component fields receive their values.

A subtotal must be positive. An empty coupon string means no coupon; a null coupon is rejected. These checks ensure that each constructed request has a positive subtotal and a coupon string that is not null. Coupon eligibility is still a handler decision.

The result records the selected rule and the calculated discount amount. Its shared helper uses decimal arithmetic and rounds the discount to two decimal places:

import java.math.BigDecimal;
import java.math.RoundingMode;

public record Discount(String rule, BigDecimal amount) {
    public static Discount percentage(String rule, BigDecimal subtotal, String rate) {
        BigDecimal amount = subtotal.multiply(new BigDecimal(rate))
                .setScale(2, RoundingMode.HALF_UP);
        return new Discount(rule, amount);
    }
}

The helper multiplies the subtotal by a decimal rate such as 0.10. Constructing BigDecimal from a string preserves the intended decimal value. RoundingMode.HALF_UP is the example’s rounding policy; applications should use the policy required for their own calculations.

3.2. Implementing the Shared Forwarding Rule

Every handler needs the same control flow: try its rule, return a successful result, or delegate. The DiscountHandler base class keeps that behavior in one place:

import java.util.Objects;
import java.util.Optional;

public abstract class DiscountHandler {
    private final DiscountHandler next;

    protected DiscountHandler(DiscountHandler next) {
        this.next = next;
    }

    public final Optional<Discount> handle(DiscountRequest request) {
        Objects.requireNonNull(request, "request is required");
        Optional<Discount> result = tryHandle(request);
        if (result.isPresent()) {
            return result;
        }
        return next == null ? Optional.empty() : next.handle(request);
    }

    protected abstract Optional<Discount> tryHandle(DiscountRequest request);
}

tryHandle() is the operation that concrete handlers implement. Returning an empty Optional means that this handler did not select a discount. It does not mean the checkout itself failed. Every implementation must return an Optional instance, never null.

handle() is final, so subclasses cannot override the shared stopping rule. A present result is returned before the successor call. If the result is empty and a successor exists, next.handle(request) continues the chain. If no successor exists, the empty result goes back to the sender.

The successor reference is private final and is set by the constructor. The example has no method for changing links after construction. Reconfiguring the chain means constructing another chain.

3.3. Implementing the Coupon Handler

The coupon handler recognizes one exact code. It returns the calculated discount when that code matches and an empty result otherwise:

import java.util.Optional;

public final class CouponHandler extends DiscountHandler {
    public CouponHandler(DiscountHandler next) {
        super(next);
    }

    @Override
    protected Optional<Discount> tryHandle(DiscountRequest request) {
        if ("SAVE10".equals(request.couponCode())) {
            return Optional.of(Discount.percentage("Coupon", request.subtotal(), "0.10"));
        }
        return Optional.empty();
    }
}

The comparison is case-sensitive: SAVE10 qualifies, while save10 does not. The handler does not need to know that a loyalty handler follows it. It only reports whether its own rule produced a result; the base class performs any forwarding.

3.4. Implementing the Loyalty Handler

The loyalty handler checks the membership flag and applies its own rate:

import java.util.Optional;

public final class LoyaltyHandler extends DiscountHandler {
    public LoyaltyHandler(DiscountHandler next) {
        super(next);
    }

    @Override
    protected Optional<Discount> tryHandle(DiscountRequest request) {
        if (request.member()) {
            return Optional.of(Discount.percentage("Loyalty", request.subtotal(), "0.05"));
        }
        return Optional.empty();
    }
}

This handler can serve as the end of the chain or be followed by another handler. Its eligibility logic is unchanged in either position. What changes is whether an earlier handler has already accepted the request before it gets a chance to run.

3.5. Connecting the Handlers

Build the chain from its end, then retain the first handler as the entry point:

DiscountHandler loyalty = new LoyaltyHandler(null);
DiscountHandler chain = new CouponHandler(loyalty);

The first statement creates the final handler. The second creates the coupon handler with that existing handler as its successor. Calling chain.handle(request) therefore checks the coupon first and membership only when the coupon rule declines.

CouponHandler returns a 10 percent discount when SAVE10 matches; otherwise LoyaltyHandler checks membership and returns 5 percent or no discount.

These handlers calculate a result without changing the request or storing per-request state. The same chain can process another request using the same rules. The pattern itself does not require handlers to be stateless; shared mutable state would need its own concurrency design.

4. Running Different Requests

The complete ChainDemo submits requests and prints each selected discount. From the repository root, run:

javac -d out src/main/java/com/howtodoinjava/patterns/behavioral/chainofresponsibility/*.java
java -cp out com.howtodoinjava.patterns.behavioral.chainofresponsibility.ChainDemo

The demo uses this helper to turn the optional result into readable output:

private static void show(String label, Optional<Discount> result) {
    String description = result
            .map(discount -> discount.rule() + " discount " + discount.amount())
            .orElse("No discount");
    System.out.println(label + ": " + description);
}

map() formats a present discount. orElse() supplies the text for an empty result. The method belongs inside ChainDemo; it does not participate in selecting a handler.

4.1. Handling a Coupon, Membership and No Match

Use the coupon-first chain with three requests that all have a subtotal of 100.00:

DiscountHandler chain = new CouponHandler(new LoyaltyHandler(null));
DiscountRequest both = new DiscountRequest(new BigDecimal("100.00"), "SAVE10", true);
show("Coupon and member", chain.handle(both));
show("Member only", chain.handle(
        new DiscountRequest(new BigDecimal("100.00"), "", true)));
show("No eligible rule", chain.handle(
        new DiscountRequest(new BigDecimal("100.00"), "", false)));

The output is:

Coupon and member: Coupon discount 10.00
Member only: Loyalty discount 5.00
No eligible rule: No discount

The first request stops at the coupon handler. The second passes through it because the coupon is absent, then stops at the loyalty handler. The third reaches the end with no selected discount. The caller can continue checkout at the original subtotal in that case.

4.2. Reversing the Order

Now build a loyalty-first chain and submit the same both request:

DiscountHandler reversed = new LoyaltyHandler(new CouponHandler(null));
show("Reversed order", reversed.handle(both));

The result changes:

Reversed order: Loyalty discount 5.00

The membership rule now accepts the request before the coupon rule runs. This chain selects the first eligible discount, not the largest discount. Finding the largest amount would require evaluating all eligible rules and comparing their results. That is a different selection policy from the first-match chain implemented here.

4.3. Rejecting an Invalid Request

A zero subtotal is rejected before it can enter the chain:

try {
    new DiscountRequest(BigDecimal.ZERO, "", false);
} catch (IllegalArgumentException exception) {
    System.out.println("Invalid request: " + exception.getMessage());
}

The output is:

Invalid request: subtotal must be positive

This is different from the valid request that receives no discount. Invalid request data causes an exception during construction. An ineligible discount request travels through the chain and returns an empty result normally.

5. When to Use a Chain

A chain is useful when several handlers can respond to the same request and their order or composition can vary. Each handler owns one decision, while the sender depends only on the common operation. Adding another discount rule requires a handler and a change to the chain’s construction; it does not require changing the existing handlers.

The additional classes are not always worthwhile. If the rules are few, fixed and used in one place, a straightforward conditional may be easier to maintain. Long chains also make it harder to trace why a particular handler won, so tests should cover ordering, short-circuiting and the no-match case.

Related designs place the decision in different locations:

Design How processing is selected
Chain of Responsibility Each handler decides whether to handle or forward the request.
Strategy The caller or configuration selects an algorithm before invoking it.
Command A request is represented as an operation that can be invoked, queued or recorded.

Jakarta Servlet’s FilterChain.doFilter() lets a filter pass the request and response to the next filter or the target resource. A filter may perform work both before and after that call, or choose not to continue. That differs from our chain, where a selected discount returns immediately and the remaining handlers do not run.

6. Frequently Asked Questions

The important choices are the stopping rule, the result returned to the sender, and how application setup constructs the chain. The pattern does not prescribe one API for every application.

6.1. Can More Than One Handler Process a Request?

Yes, if the application defines a chain that continues after processing. Validation filters and logging stages often do that. In this example, a successful discount ends processing to enforce the no-stacking policy. Changing that rule requires changing both the control flow and the result model, rather than simply adding another handler.

6.2. Must Handlers Store a Next Reference?

No. A coordinator can iterate over an ordered list of handlers and stop at the first result. That approach keeps traversal in one place. Explicit successor references are useful when handlers need control over delegation or work around the successor call. Choose the representation that fits the required behavior.

6.3. Can a Chain Contain a Cycle?

A chain built with mutable links can accidentally point back to an earlier handler. A request that keeps being forwarded would then fail to reach an end. Our handlers receive their successor only during construction, and the shown setup links newly created handlers to already constructed successors. There is no setter that can reconnect them into a cycle. If links are configurable, validate the resulting chain before using it.

7. Conclusion

Use Chain of Responsibility when the sender should offer a request to several potential handlers without choosing the receiver itself. Define the order, stopping rule and no-match result explicitly. Those decisions determine the runtime behavior and are the most important cases to verify when the chain changes.

Happy Learning!

The complete example is available in the GitHub source folder.

Leave a Comment

  1. Very helpful guide!

    I’d also like to add that anytime you are repeating code, it is worth the time for further evaluation.

    The support services… DirectorSupport, FrontDeskSupport, ManagerSupport, SupervisorSupport could all be lifted to an abstract class.

    public abstract class AbstractSupport implements ISupportService
    {
        private ISupportService next = null;
    
        private final ServiceLevel serviceLevel;
    
        public AbstractSupport(ServiceLevel serviceLevel) {
            this.serviceLevel = serviceLevel;
        }
    
        public boolean isServiceLevel(ServiceLevel serviceLevel) {
            return this.serviceLevel == serviceLevel;
        }
    
        public final ISupportService getNext()
        {
            return next;
        }
    
        public final void setNext(ISupportService next)
        {
            this.next = next;
        }
    
        public void handleRequest(ServiceRequest request) {
            if (isServiceLevel(request.getType())) {
                handleRequestImpl(request);
            } else {
                if (getNext() != null) {
                    getNext().handleRequest(request);
                }
                else
                {
                    request.setConclusion("Your problem is none of our business");
                    throw new IllegalArgumentException("You problem is none of our business :: " + request.getType());
                }
            }
        }
    
        public abstract void handleRequestImpl(ServiceRequest request);
    }
    

    By doing this you can simplify the required code in the subclasses:

    public class FrontDeskSupport extends AbstractSupport
    {
        public FrontDeskSupport()
        {
            super(ServiceLevel.LEVEL_ONE);
        }
    
        public void handleRequestImpl(ServiceRequest request)
        {
            request.setConclusion("Front desk handled request - Level: " + request.getType());
        }
    }
    
    public class SupervisorSupport extends AbstractSupport
    {
        public SupervisorSupport()
        {
            super(ServiceLevel.LEVEL_TWO);
        }
    
        public void handleRequestImpl(ServiceRequest request)
        {
            request.setConclusion("Supervisor support handled request - Level: " + request.getType());
        }
    }
    
    public class ManagerSupport extends AbstractSupport
    {
        public ManagerSupport()
        {
            super(ServiceLevel.LEVEL_THREE);
        }
    
        public void handleRequestImpl(ServiceRequest request)
        {
            request.setConclusion("Manager support handled request - Level: " + request.getType());
        }
    }
    
    
    public class DirectorSupport extends AbstractSupport {
    
        public DirectorSupport()
        {
            super(ServiceLevel.LEVEL_FOUR);
        }
    
        public void handleRequestImpl(ServiceRequest request)
        {
            request.setConclusion("Director handled request - Level: " + request.getType());
        }
    }
    

    Of course, this could be even further expanded and made more dynamic by specifying the service level types in the config XML file for each support class. I’ll leave that to the reader’s own ideas.

  2. Hi Lokesh,
    Thank you so much for the explanation.
    Why is the SupportService class implementing the “SupportServiceItf ” interface ? Can’t we directly inject the object of SupportService and use it , wherein in supportService class takes care of directing the request to frontDeskService and so on as you mentioned. Is it the pattern that we should follow in Chain of responsibility pattern ?

  3. Also, the chain of responsibility is sometimes better handled with the handleRequest(…) method returning a boolean: true if the request is handled, false otherwise.

    public interface SupportServiceItf
    {
    public boolean handleRequest(ServiceRequest request);
    }

    Then, a class possessing a Collection of the handlers, the call is simply

    boolean isHandled = false;

    for(SupportServiceItf handler : collectionOfHanders){
    isHandled = handler.handle(serviceRequest);
    if(isHandled){
    break;
    }
    }

    if(!isHandled){
    // something may need to be done because the request never got handled
    }

    This also avoids handlers being aware of each other in what is essentially a singly linked list. I believe that it is generally better that the handlers be unaware of each other’s existence and be isolated from each other.

    There is also the additional difficulty that the classes have paired getters and setters: getNext()/setNext(…) and getHandler)/setHandler(…). Once a field of a class is made read/write, the field is now public. For safety, I believe that this example would be made better if the classes were made immutable.

    However, if the approach of putting the handler in a Collection were used, then the handler code is simplified. Any re-arrangements can be done by a management class, which I believe to be a much more flexible approach.

      • But if Management class will be delegating responsibility from one class t another then delegator design pattern or kind of factory design pattern

        Agree there is tight coupling in class that is not good.We have to put example that will has High cohesion and low Coupling.

        one more Question Struts 2 is based on this pattern??/

        • I have agreed only on “immutable” part. It makes sense to me make handlers immutable. I also feel that Management class will not add any benefit here.

          Tight coupling? I will not agree easily. This design has lots of benefit e.g. directly passing request to certain handler and a request handled by even multiple handlers.(You may come across this situation also where request may need multiple handlers.) In code there is no references to other classes. Everything is done in configuration file. Where is tight coupling? You have configure your system somewhere, right?

          I really do not find struts 2 a good candidate for example of this pattern. Reason? CoR pattern has multiple handlers and one of them handle the request. In struts2, actual handler is Action class and interceptors actually works as decorators or observers and sometimes doing some more work. They are more file filters in servlet technology. They do not handle the request. So, I will prefer not to mention struts 2 as example of chain of responsibility pattern.

  4. The example would be better by reconsidering the design of the ServiceRequest class. By having both a get a set method for each of its private fields type and conclusion, its encapsulation is broken. The fields are effectively public. I believe that the ServiceRequest ought to be made invariant. The fields are final and private. They are set through the constructor. The set methods are removed, leaving only the get methods. As it stands, and handler could accidentally change the service request resulting in an error which requires debugging.

  5. I have couple of Questions.
    1 Who will define next level.
    2. What will be the flow of request. FrontDesk -> Manager or FrontDesk -> SupportService -> Manger ?

      • Lets say in our application we have 5 different type of chains and those may got change on frequent basics so as per your implantation we have to update next level for all the supervisor each time… if we are not using Spring in that app…
        2. What will be implementations for 5 chains..

        • How first chain will be different from other four chains. Sequence.. right?? But sequence doesn’t matter here. Concept is that every object in chain will get it’s chance to handle the request. Whoever is capable, will handle it. Otherwise just forward to next handler and let him try.

    • I have reed the comments but did not understand this point:
      “1 Who will define next level”

      If we define:

      ServiceRequest request = new ServiceRequest();
      request.setType(ServiceLevel.LEVEL_THREE);
      supportService.handleRequest(request);
      System.out.println(request.getConclusion());

      Does the execution of the application start on FrontDeskSupport (LEVEL_ONE) then to SupervisorSupport (LEVEL_TWO) and finally ManagerSupport (LEVEL_THREE) in this order?

      if the enum ServiceLevel
      {
      LEVEL_FOUR, LEVEL_THREE, LEVEL_TWO, LEVEL_ONE

      will the execution be LEVEL_FOUR, LEVEL_THREE, LEVEL_TWO, LEVEL_ONE?

      Thank you

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.