← All patterns

Chain of Responsibility

Behavioral · view source on GitHub

Read this in: English · Português · Español

Category: Behavioral

The problem

A request might need to be handled by one of several possible handlers, but the sender shouldn't have to know which one, or hard-code the decision logic for picking it. A single if/else if chain checking every handler's eligibility condition works at first, but it puts every handler's business rule in one place, coupled to every other handler's rule, and adding a new handler means editing that shared method.

The solution

Chain the handlers together, each holding a reference to the next. Each handler decides for itself whether it can (or should) handle the request; if not, it passes the request along. The sender only ever talks to the first link — it doesn't know how long the chain is, or which link actually processes the request.

classDiagram
    class Handler {
        -next
        +handle(request)
    }
    class ConcreteHandlerA
    class ConcreteHandlerB
    class ConcreteHandlerC
    Handler <|-- ConcreteHandlerA
    Handler <|-- ConcreteHandlerB
    Handler <|-- ConcreteHandlerC
    ConcreteHandlerA --> ConcreteHandlerB : next
    ConcreteHandlerB --> ConcreteHandlerC : next

Classic example

classic/Approver is the canonical purchase-approval chain: SupervisorManagerDirector, each with its own approval ceiling. A request for an amount within the Supervisor's limit never reaches the Manager at all; a request beyond every link's limit falls off the end of the chain with a clear "no approver available" result rather than an exception or a silent no-op. ApproverTest covers an amount stopping at each of the three tiers, plus the beyond-everyone case.

Applied example: transaction compliance pipeline

applied/ComplianceHandler chains KycHandlerAmlHandlerLimitHandlerFraudHandler — identity verification before watchlist screening before the business limit check before the (more expensive) fraud heuristic, mirroring how a real compliance pipeline is actually ordered: cheapest, most-decisive checks first. The first handler to reject a transaction stops the chain immediately; later handlers never even see it, which is exactly what keeps, say, the fraud heuristic from running on a transaction that was never going to pass KYC anyway. ComplianceHandlerTest covers a transaction clearing every check, and each individual handler being the one to reject.

When not to use it

Test coverage

100% instruction coverage, 100% branch coverage (JaCoCo). Reproduce it yourself:

./gradlew :behavioral:chainofresponsibility:jacocoTestReport

Report at behavioral/chainofresponsibility/build/reports/jacoco/test/html/index.html.

Further reading

Unit tests

src/test/java/com/designpatterns/behavioral/chainofresponsibility/classic/ApproverTest.java
package com.designpatterns.behavioral.chainofresponsibility.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class ApproverTest {

    private final Approver chain = new Supervisor().next(new Manager().next(new Director()));

    @Test
    void aSmallAmountStopsAtTheSupervisor() {
        assertThat(chain.approve(500_00L)).isEqualTo("Supervisor approved 50000 cents");
    }

    @Test
    void aMidSizedAmountEscalatesPastTheSupervisorToTheManager() {
        assertThat(chain.approve(5_000_00L)).isEqualTo("Manager approved 500000 cents");
    }

    @Test
    void aLargeAmountEscalatesAllTheWayToTheDirector() {
        assertThat(chain.approve(50_000_00L)).isEqualTo("Director approved 5000000 cents");
    }

    @Test
    void anAmountBeyondEveryLinksLimitFallsOffTheEndOfTheChain() {
        assertThat(chain.approve(1_000_000_00L)).isEqualTo("No approver available for 100000000 cents");
    }
}
src/test/java/com/designpatterns/behavioral/chainofresponsibility/applied/ComplianceHandlerTest.java
package com.designpatterns.behavioral.chainofresponsibility.applied;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class ComplianceHandlerTest {

    private static final long LIMIT_CENTS = 50_000_00L;

    private final ComplianceHandler chain =
            new KycHandler().next(new AmlHandler().next(new LimitHandler(LIMIT_CENTS).next(new FraudHandler())));

    @Test
    void approvesATransactionThatClearsEveryCheck() {
        ComplianceTransaction transaction = new ComplianceTransaction("payer-1", 10_000_00L, true, false, false);

        ComplianceResult result = chain.check(transaction);

        assertThat(result.approved()).isTrue();
        assertThat(result.reason()).isNull();
    }

    @Test
    void anUnverifiedPayerIsRejectedByKycBeforeAnyLaterCheckRuns() {
        ComplianceTransaction transaction = new ComplianceTransaction("payer-2", 10_000_00L, false, true, true);

        ComplianceResult result = chain.check(transaction);

        assertThat(result.approved()).isFalse();
        assertThat(result.reason()).isEqualTo("KYC: payer not verified");
    }

    @Test
    void aWatchlistedPayerIsRejectedByAml() {
        ComplianceTransaction transaction = new ComplianceTransaction("payer-3", 10_000_00L, true, true, false);

        ComplianceResult result = chain.check(transaction);

        assertThat(result.reason()).isEqualTo("AML: payer is on a watchlist");
    }

    @Test
    void anAmountAboveTheThresholdIsRejectedByTheLimitHandler() {
        ComplianceTransaction transaction = new ComplianceTransaction("payer-4", 60_000_00L, true, false, false);

        ComplianceResult result = chain.check(transaction);

        assertThat(result.reason()).isEqualTo("LIMIT: amount exceeds the 5000000 cent threshold");
    }

    @Test
    void aHighRiskFlaggedTransactionThatClearsEverythingElseIsRejectedByFraud() {
        ComplianceTransaction transaction = new ComplianceTransaction("payer-5", 10_000_00L, true, false, true);

        ComplianceResult result = chain.check(transaction);

        assertThat(result.reason()).isEqualTo("FRAUD: transaction flagged as high risk");
    }
}

View full JaCoCo coverage report →