Chain of responsibility

Part of the free Low-Level Design course on LogicWiz, in the chapter: Behavioural patterns II. The class-diagram exercises run in the app.

The validate method nobody wanted to touch

"Ops wants the fraud check to run last, and medicine orders need a prescription check. How many places do I edit?"

Arjun, asking Priya before a deadline


Five checks in one method

Before SwiftKart accepts an order, it runs five checks:

  • Is every item in stock?
  • Is the delivery pincode serviceable? A pincode is the six-digit Indian postal code, and serviceable means SwiftKart delivers there.
  • Is the coupon valid for this order?
  • Is the payment within limits? Cash on delivery is allowed only up to 2,000 rupees.
  • Does the fraud check pass? A separate service gives each order a risk score.

Here is a short version of Arjun's code:

public class OrderValidator {
    // stock, area, coupons and fraud are set in the constructor

    public String validate(Order order) {
        for (Item item : order.items()) {
            if (stock.available(item.sku()) < item.qty()) {
                return "Out of stock: " + item.sku();
            }
        }
        if (!area.serves(order.pincode())) {
            return "We do not deliver to " + order.pincode() + " yet";
        }
        if (order.coupon() != null && !coupons.isValid(order.coupon(), order)) {
            return "This coupon cannot be used on this order";
        }
        if (order.isCashOnDelivery() && order.total() > 2000) {
            return "Cash on delivery is limited to 2,000 rupees";
        }
        if (fraud.score(order) > 80) {
            return "Order held for review";
        }
        return null;
    }
}

Now the requests arrive:

  • Medicine orders need a prescription check.
  • Store pickup orders should skip the pincode check, because nobody delivers them.
  • The fraud check calls a slow service, so it should run last.
  • The checkout page wants to show every problem at once, not only the first.

Every request edits this one method. The checks cannot be tested alone: to test the pincode rule you also need a stock service and a fraud service. The class depends on four services for five unrelated jobs.

The idea in plain words

Think of an expense claim in an office. You hand the claim to your team lead. If it is under 5,000 rupees, she signs it. If not, she passes it to the manager. Above 50,000 rupees, the manager passes it to finance. You do not know who will finally sign. You only know the first person in the line.

Each approver checks one thing and then does one of two things: handles the claim, or passes it on. Adding an approver, such as a legal review for contracts, does not change anyone else's job.

The Chain of Responsibility pattern works the same way in code. A request travels along a chain of handlers. A handler is a small object that does one check or one job. Each handler decides to handle the request, reject it, or pass it to the next handler. The sender knows only the first handler.

Where the analogy stops working: in the office, usually one person handles the claim and the rest never see it. In order validation, every handler looks at the order unless one rejects it first. Both are chains; they differ in when the chain stops. Also, the office line is fixed by the company's structure. In code, you build the chain yourself, and different orders can get different chains.

The structure

The pattern has three roles:

  • Handler: an abstract class (or interface) with a method to handle the request and a link to the next handler.
  • Concrete handlers: one class per check, such as StockCheck and PincodeCheck.
  • Client: the code that builds the chain and gives the request to the first handler. Here it is CheckoutService.
classDiagram
  class OrderCheck {
    <<abstract>>
    -next: OrderCheck
    +then(next) OrderCheck
    +handle(order) CheckResult
    #check(order) CheckResult
  }
  CheckoutService --> OrderCheck : first
  OrderCheck <|-- PincodeCheck
  OrderCheck <|-- StockCheck
  OrderCheck <|-- CouponCheck
  OrderCheck <|-- FraudCheck

This is how an order with a bad coupon flows. It never reaches the payment or fraud checks:

sequenceDiagram
  participant C as CheckoutService
  participant P as PincodeCheck
  participant S as StockCheck
  participant K as CouponCheck
  C->>P: handle(order)
  P->>S: handle(order)
  S->>K: handle(order)
  K-->>S: fail, coupon expired
  S-->>P: fail
  P-->>C: fail

A handler passes the order on by calling the next handler's handle. The result travels back along the same path to the checkout.

Build it step by step

Step 1: a result type and the abstract handler.

public record CheckResult(boolean ok, String reason) {
    public static CheckResult pass() { return new CheckResult(true, null); }
    public static CheckResult fail(String reason) { return new CheckResult(false, reason); }
}

public abstract class OrderCheck {
    private OrderCheck next;

    public OrderCheck then(OrderCheck next) {
        this.next = next;
        return next;
    }

    public final CheckResult handle(Order order) {
        CheckResult result = check(order);
        if (!result.ok() || next == null) {
            return result;
        }
        return next.handle(order);
    }

    protected abstract CheckResult check(Order order);
}

A record is a short Java class that only holds data. CheckResult says whether a check passed, and why not if it failed. The handle method is the chain itself. It runs this handler's check. If the check fails, or there is no next handler, it returns. Otherwise it passes the order along. It is final, so no handler can break the passing rule.

Step 2: each handler fills in one check.

public class PincodeCheck extends OrderCheck {
    private final ServiceArea area;

    public PincodeCheck(ServiceArea area) { this.area = area; }

    protected CheckResult check(Order order) {
        if (area.serves(order.pincode())) {
            return CheckResult.pass();
        }
        return CheckResult.fail("We do not deliver to " + order.pincode() + " yet");
    }
}

PincodeCheck needs only ServiceArea. Its test needs no stock service and no fraud service.

Step 3: build the chain once, in the order you want.

OrderCheck first = new PincodeCheck(area);
first.then(new StockCheck(stock))
     .then(new CouponCheck(coupons))
     .then(new PaymentLimitCheck(2000))
     .then(new FraudCheck(fraud));

CheckResult result = first.handle(order);

then returns the handler it was given, so the calls read top to bottom in the order the checks run. Keep a reference to the first handler, as first does here. The value at the end of the calls is FraudCheck, the last link. Calling handle on it would skip every other check.

Choosing the order. Put cheap checks first and slow ones last. The pincode check is a lookup in memory; the fraud check calls another service. If the order cannot be delivered, there is no reason to pay for a fraud score. Also put any check that reserves something, such as holding stock, at the end. Otherwise a later failure leaves a reservation that someone must undo.

Different chains for different orders. A small builder method can return a chain per order type. A medicine order gets a PrescriptionCheck added. A pickup order gets a chain without PincodeCheck. No existing handler changes.

Stop early, or collect every error? The chain above stops at the first failure. That is right when later checks make no sense after a failure. The checkout page, though, wants to show "coupon expired" and "cash limit crossed" together, so the customer fixes both at once. For that, pass a List<String> errors along with the order. Each handler adds its error to the list and always calls the next one. Pick one style per chain, and say which in an interview.

When a chain helps, and when a list is enough

A chain fits when:

  • a request passes through several independent checks or steps,
  • their order matters, or changes by request type,
  • a handler must decide whether the next one runs at all.

It has costs. The flow is harder to see, because you must read the building code to know the order. A missing then call drops every check after it, with no error. Debugging needs a log line in each handler.

In a mock interview, a candidate said: "A chain is a list of validators and a for loop, with extra steps." For plain validation, the candidate is mostly right. A List<OrderCheck> and a loop that stops at the first failure is often clearer. Many teams write exactly that, and it is a good interview answer.

The linked chain earns its place when a handler controls what happens next. It may handle the request and stop, as an approver does. It may change the request before passing it on. It may also run code both before and after the rest of the chain, such as timing every later check. A loop cannot do these without growing its own if statements.

You met the decorator in the chapter on structural patterns. It also wraps the next object, but a decorator always calls it and adds behaviour. A handler in a chain may stop the request.

💡 Tip: In the classic flavour, where one handler deals with the request, add a last handler that catches anything nobody handled. Otherwise a request can fall off the end and vanish.

Chains appear in several interview problems. In an ATM cash dispenser, the 2,000 rupee handler gives as many notes as it can, then passes the rest to the 500 handler, then the 100 handler. A logging framework passes each message along loggers by level. Support tickets escalate from agent to team lead to manager. You will also meet a pricing chain in the shopping cart problem.

Practice: split the validator into a chain

The starter is Arjun's OrderValidator with every check as a private method. Turn it into an abstract handler that links to the next handler, with at least three check classes. CheckoutService should know only the handler type.

📌 Summary: A chain passes a request through small handlers; each checks one thing, then stops it or passes it on. Put cheap checks first. For plain validation, a list and a loop is often enough.

What Arjun learns next

Checks are now small and easy to reorder. Next, Priya opens the weekly payout code for riders and dark stores. The two jobs share five steps, copied twice, and a tax fix reached only one of them. That is the template method pattern.

Check your understanding

  1. Medicine orders need an extra prescription check. With the chain in place, what changes?
  2. The checkout page wants to show the coupon error and the cash limit error together. What should change?
  3. The fraud check calls a slow service. The pincode check is a lookup in memory. Where should each go?
  4. A candidate says: "A chain is a list of validators and a for loop, with extra steps." When does the linked chain earn its place?