Low-level designCore9 min

Chain of Responsibility

Pass a request along a chain of handlers, each of which can deal with it, stop it, or hand it to the next. Middleware is the everyday form: each check is a handler, and each endpoint is the chain of checks it needs.

Watch it evolve1 / 9
The problem

An API with checks in each endpoint

GardenApi checks the rate limit, and for bookings membership, at the top of each endpoint method.

80
Start
Stage 1
Stage 2
Final

1 · The situation

An API with checks in front of each endpoint

A community garden lets members book plots through a small web API. Two endpoints matter here: booking a plot, which only members may do, and listing free plots, which anyone may see. Both are rate-limited to 2 requests a minute per caller, so a script can't grab every plot.

The first version puts the checks at the top of each endpoint method in GardenApi: the rate limit, then, for bookings, membership. A Limiter counts requests per caller. Python writes book_plot() and list_plots().

The first versionJava
import java.util.HashMap;
import java.util.Map;

record Request(String member, String path) {
    String who() {
        return member.isEmpty() ? "guest" : member;
    }
}

class Limiter {
    private final int perMinute;
    private final Map<String, Integer> counts = new HashMap<>();

    Limiter(int perMinute) {
        this.perMinute = perMinute;
    }

    boolean allow(String who) {
        return counts.merge(who, 1, Integer::sum) <= perMinute;
    }
}

class GardenApi {
    private final Limiter limiter;

    GardenApi(Limiter limiter) {
        this.limiter = limiter;
    }

    String bookPlot(Request r) {
        if (!limiter.allow(r.who())) return "429 slow down";
        if (r.member().isEmpty()) return "401 members only";
        return "200 booked";
    }

    String listPlots(Request r) {
        if (!limiter.allow(r.who())) return "429 slow down";
        return "200 plots 7 and 9 are free";
    }
}

public class Main {
    static void show(Request r, String response) {
        System.out.println(r.who() + " " + r.path() + " -> " + response);
    }

    public static void main(String[] args) {
        GardenApi api = new GardenApi(new Limiter(2));
        Request[] bookings = {
            new Request("Ana", "POST /plots/7"),
            new Request("", "POST /plots/8"),
        };
        for (Request r : bookings) show(r, api.bookPlot(r));
        Request list = new Request("", "GET /plots");
        show(list, api.listPlots(list));
        for (String plot : new String[] {"9", "12"}) {
            Request r = new Request("Ana", "POST /plots/" + plot);
            show(r, api.bookPlot(r));
        }
    }
}
«record»Request− member: String− path: String~ who(): StringLimiter− perMinute: int− counts: Map<String, Integer>~ allow(String): booleanGardenApi− limiter: Limiter~ bookPlot(Request): String~ listPlots(Request): String

The class diagram of this version, drawn from its code: one class holds every endpoint and every check.

A member books, a guest tries to, a guest lists the plots, and the member books twice more:

Ana POST /plots/7 -> 200 booked
guest POST /plots/8 -> 401 members only
guest GET /plots -> 200 plots 7 and 9 are free
Ana POST /plots/9 -> 200 booked
Ana POST /plots/12 -> 429 slow down

2 · The change

A new check for every endpoint

The garden's host takes the API down for maintenance every night, and wants every endpoint to answer "503 down for maintenance" first while it's down. Look at where checks live today:

Each endpoint runs its own checksJava
class GardenApi {
    private final Limiter limiter;

    GardenApi(Limiter limiter) {
        this.limiter = limiter;
    }

    String bookPlot(Request r) {
        if (!limiter.allow(r.who())) return "429 slow down";
        if (r.member().isEmpty()) return "401 members only";
        return "200 booked";
    }

    String listPlots(Request r) {
        if (!limiter.allow(r.who())) return "429 slow down";
        return "200 plots 7 and 9 are free";
    }
}

Each endpoint method starts with a list of checks, and the lists differ by endpoint:

Endpoint

Checks today

With maintenance

Book a plot

Rate limit, members only

Maintenance, rate limit, members only

List plots

Rate limit

Maintenance, rate limit

A planned endpoint to swap plots

Not written yet

Maintenance, rate limit, members only, again

Every new check edits every endpoint method, and every new endpoint copies the right checks in the right order. The order matters, too: a maintenance check placed after the rate limit would count requests the API was never going to serve.

3 · The refactor

Make each check a link in a chain

Start from the first version. Each check does the same thing: look at the request, and either answer it or let it through. Give that one interface, and let each check hold the next one:

Handler has one method, handle(request). Step is a base class that holds the next handler. RateLimit and MembersOnly each answer with an error, or return what the next handler returns. Each rate limit keeps its own Limiter.

Java
abstract class Step implements Handler {
    protected final Handler next;

    Step(Handler next) {
        this.next = next;
    }
}

class MembersOnly extends Step {
    MembersOnly(Handler next) {
        super(next);
    }

    public String handle(Request r) {
        if (r.member().isEmpty()) return "401 members only";
        return next.handle(r);
    }
}
1«record»Request− member: String− path: String~ who(): StringLimiter− perMinute: int− counts: Map<String, Integer>~ allow(String): boolean«interface»Handler+ handle(Request): String«abstract»Step# next: HandlerRateLimit− limiter: Limiter+ handle(Request): StringMembersOnly+ handle(Request): StringBookPlot+ handle(Request): StringListPlots+ handle(Request): String

After the second step the program prints what it printed before. After the third, the site goes down and both endpoints answer 503:

Ana POST /plots/7 -> 200 booked
guest POST /plots/8 -> 401 members only
guest GET /plots -> 200 plots 7 and 9 are free
Ana POST /plots/9 -> 200 booked
Ana POST /plots/12 -> 429 slow down
guest GET /plots -> 503 down for maintenance
Ben POST /plots/3 -> 503 down for maintenance
Checks inside each endpoint
2
Checks as handlers
0

Existing methods edited to add the maintenance check. Before: both endpoint methods. After: none; Maintenance is new, and main links it in.

4 · The pattern

Chain of Responsibility: handle it or pass it on

Chain of Responsibility sends a request along a chain of handlers. Each handler decides whether to deal with the request, which ends the chain, or to pass it to the next handler. The sender knows only the first handler, and doesn't know which one will answer.

1«record»Request− member: String− path: String~ who(): StringLimiter− perMinute: int− counts: Map<String, Integer>~ allow(String): booleanSiteStatus− down: boolean~ setDown(boolean)~ down(): boolean«interface»Handler+ handle(Request): String«abstract»Step# next: HandlerRateLimit− limiter: Limiter+ handle(Request): StringMembersOnly+ handle(Request): StringMaintenance− down: BooleanSupplier+ handle(Request): StringBookPlot+ handle(Request): StringListPlots+ handle(Request): String

The roles in the garden's code: Handler is the handler interface, Step holds the link to the next one, and each check or endpoint is a concrete handler.

Role

In the garden API

Its job

Handler

Handler

The interface every link implements: handle(request)

Concrete handler

Maintenance, RateLimit, MembersOnly

Answers the request, or passes it to the next link

Final handler

BookPlot, ListPlots

Does the work when every check has passed

Client

main

Builds the chains and sends requests to the first link

A guest's booking passes maintenance and the rate limit, and is stopped by MembersOnly; BookPlot is never called.

Two flavors share the name. In the classic form, exactly one handler deals with the request and the rest pass it along, like levels of support escalation. In the middleware form, used here, every link may do some work, such as counting a request, and any link may stop it; the request reaches the end only if nothing stopped it.

5 · What it costs

Order matters, and nothing promises an answer

  • Order is part of the behavior. Maintenance must come before the rate limit, or closed-site requests use up a caller's quota. The order lives in the code that builds the chain, so that's where it has to be right.
  • A request can fall off the end. If no handler answers, the chain needs a final handler or a default; in Express, a middleware that neither responds nor calls next() leaves the request hanging.
  • Debugging spans many classes. Finding out why a request was refused means walking the chain.
  • Every link adds a call. For a few checks it's nothing; for hundreds of handlers on a hot path, it shows.

Situation

Chain?

A request passes several checks, and the set or order differs by case

Yes

One check in one place

No: an if at the top of the method

Every step must run, in a fixed order, always

A plain sequence of calls, or a Template Method

Several handlers must all react, and none can stop the others

Observer

6 · Getting it right

One job per link, assembled in one place

  • Give each handler one job. RateLimit limits, MembersOnly checks membership. A handler that does both can't be reused on a public endpoint.
  • Build the chains in one place. main here, a router or app setup in a real server. Reading it shows every endpoint's checks in order.
  • End every chain. The last link should always answer, so no request goes unhandled.
  • Keep handlers independent. A handler should depend on the request and its own collaborators, not on which handlers come before or after it.
  • Pass functions where a link only needs a yes or no. Maintenance takes a check function rather than the status object, so it works with any source of the answer.

Language

The usual shape

Java

An interface and a base class holding next; servlet filters pass a request on through FilterChain.doFilter()

C++

An abstract handler owning the next link through std::unique_ptr

JavaScript

Functions taking the request and a next callback, as in Express middleware, or objects with handle()

Python

Objects with handle(), or functions wrapping a handler, as in WSGI middleware

7 · Look-alikes

A link that holds the next one

Chain of Responsibility, Decorator and Proxy all hold an object of the same interface and can call it. What differs is whether the call always happens:

Pattern

Calls the object it holds

Purpose

Chain of Responsibility

Only if it doesn't handle the request itself

Let one of several handlers answer, or stop the request

Decorator

Always, adding work around the call

Add behavior

Proxy

When access is allowed, possibly later

Control access to one real object

Command

Not applicable: a command is the request itself

Store, queue or undo a request

Middleware blurs the first two: a logging middleware that always calls next() is a decorator in all but name. The pattern's defining move is the option to stop: a handler that can end the request is a link in a chain.

8 · In practice

Middleware you have already used

Web frameworks are built on this pattern. In Express, a middleware function receives the request, the response and next; it can end the request-response cycle, or call next() to pass control on, and the request is left hanging if it does neither. Java servlet filters work the same way through a FilterChain.

The practice checks recognize a Chain of Responsibility when a handler interface has implementations that hold the next handler, directly or through a base class, and pass a request on only some of the time. If every handler always passes on, the chain is reported as missing a way to end.

Check yourself

4 questions

1. The garden adds an endpoint to swap plots, for members only. What changes?
2. Why must Maintenance come before RateLimit in each chain?
3. A logging handler always calls the next handler and never stops a request. Which pattern is it closest to?
4. A chain's last link is RateLimit, and the request passes the limit. What happens?

Takeaways

Remember this

  • Chain of Responsibility passes a request along handlers, each of which answers it or passes it on.
  • Each check is a handler with one job; each endpoint is the chain of checks it needs.
  • The order of the chain is behavior: build chains in one place, and end each with a handler that always answers.
  • Middleware is the everyday form: every link may do some work, and any link may stop the request.
  • A link that always passes on is a decorator; the option to stop is what makes a chain.