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.
An API with checks in each endpoint
GardenApi checks the rate limit, and for bookings membership, at the top of each endpoint method.
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().
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));
}
}
}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:
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.
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);
}
}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
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.
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 |
| The interface every link implements: |
Concrete handler |
| Answers the request, or passes it to the next link |
Final handler |
| Does the work when every check has passed |
Client |
| 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 |
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.
RateLimitlimits,MembersOnlychecks membership. A handler that does both can't be reused on a public endpoint. - Build the chains in one place.
mainhere, 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.
Maintenancetakes 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 |
C++ | An abstract handler owning the next link through |
JavaScript | Functions taking the request and a |
Python | Objects with |
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
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.