Patterns you will actually be asked for · chapter 24 of 33
Decorator and Chain of Responsibility
Chapter 3.6 · Part 3, Patterns you will actually be asked for · about 35 minutes
What you need before this chapter: Part 1 in full, especially interfaces (1.4). Part 2 in full, especially single responsibility (2.4). Chapter 3.1, Strategy, for functional interfaces and lambdas, which both patterns in this chapter use constantly.
When you finish this chapter you will be able to:
- Recognise when cross-cutting behaviour, wired inline around one call, is tangling unrelated concerns together in one method
- Build a chain of steps out of one functional interface, wire them in a fixed order, and let each one decide to stop, pass on, or act on the way back
- Tell a pure Chain of Responsibility link from a Decorator by whether it touches what comes back
- Say when a single ordered chain is the wrong tool, and name what a per-route rule actually needs instead
1. The situation
A request pipeline needs to record every response after it is served: which method, which path, what status came back. The obvious place to write that is right after routing runs.
static Response handle(Request request) {
Response response = route(request);
accessLog.add(request.method() + " " + request.path() + " -> " + response.status());
return response;
}
2. Naive code that is fine
javac Step1.java
java Step1
GET /orders -> 200
POST /orders -> 200
For exactly one behaviour, wrapped around exactly one call, this is easy to read end to end. There is nothing to extract yet.
3. A new requirement
Two more rules arrive close together. Admin paths need a valid token, checked before routing runs at all, and rejected requests should never reach the log. Response timing needs recording too, wrapped around the same routing call, with its own before measurement taken first.
static Response handle(Request request) {
if (request.path().startsWith("/admin") && !VALID_TOKEN.equals(request.adminToken())) {
return new Response(403);
}
long start = System.nanoTime();
Response response = route(request);
boolean tookMeasurableTime = System.nanoTime() >= start;
accessLog.add(request.method() + " " + request.path() + " -> " + response.status()
+ " (timed=" + tookMeasurableTime + ")");
return response;
}
javac Step2.java
java Step2
GET /orders -> 200 (timed=true)
Only one line prints: the admin request with the wrong token was rejected before it ever reached the timing or the logging code, exactly as required. The method still works. What is worth noticing is that three unrelated concerns, security, timing, and logging, are now interleaved in one block, and turning any one of them off means finding and deleting its lines from the middle of the others.
4. Watch where it goes, and the real cost
A fourth requirement is coming, whatever it turns out to be: a rate limit, a request ID stamped onto every log line, a header added to every response. Each one behaves the same way as one of the two already here — either it decides whether the rest of the pipeline runs at all, or it wraps the call and does something with what comes back. Adding it means reading past the admin check and the timing block to find where a fourth concern belongs, in a method that three different teams now have reason to edit.
The real cost is not handle's length. It is that a security rule, a timing rule, and a logging rule have no boundary between them in the source. A change to how one of them decides to act risks breaking a check that has nothing to do with it, because they share the same lines rather than living in their own.
5. The move
Both rules turn out to be the same shape from the outside: something that receives a request and "the rest of the pipeline" as a value, and decides what to do with both. Name that shape once.
@FunctionalInterface
interface Chain {
Response next(Request request);
}
@FunctionalInterface
interface Middleware {
Response handle(Request request, Chain chain);
}
The admin check becomes a link that decides whether to call chain.next(request) at all.
final class AdminGate implements Middleware {
private final String validToken;
AdminGate(String validToken) { this.validToken = validToken; }
@Override
public Response handle(Request request, Chain chain) {
if (request.path().startsWith("/admin") && !validToken.equals(request.adminToken())) {
return new Response(403);
}
return chain.next(request);
}
}
The logging rule becomes a link that always calls chain.next(request), but does its own work with what comes back.
final class AccessLog implements Middleware {
private final List<String> lines = new ArrayList<>();
@Override
public Response handle(Request request, Chain chain) {
Response response = chain.next(request);
lines.add(request.method() + " " + request.path() + " -> " + response.status());
return response;
}
List<String> lines() { return lines; }
}
One small method wires any list of links into a single callable pipeline, in the order they are listed.
static Chain wrapping(List<Middleware> middlewares, Chain terminal) {
Chain chain = terminal;
for (int i = middlewares.size() - 1; i >= 0; i--) {
Middleware middleware = middlewares.get(i);
Chain rest = chain;
chain = request -> middleware.handle(request, rest);
}
return chain;
}
Chain pipeline = wrapping(List.of(adminGate, accessLog), terminal);
javac Step3.java
java Step3
GET /orders -> 200
GET /admin/reports -> 200
Three requests went through: an ordinary one, a rejected admin request, and an authorised one. The rejected one never appears in the log, because AdminGate runs first in the list and never calls chain.next for it, so AccessLog never sees that request at all.
These are two different patterns, wearing the same interface. AdminGate only ever decides: handle this here, or pass it on unchanged. That is Chain of Responsibility. AccessLog always passes the request on, and then does something with the response that comes back. That is Decorator: a wrapper that adds behaviour before or after a call it does not own, without changing the call's shape. The one-line test that tells them apart: does this link ever look at what chain.next handed back? AdminGate never does. AccessLog always does.
6. What modern Java changes here
The original catalogue's Chain of Responsibility usually shows an abstract Handler with a setNext(Handler) method, each subclass calling into the next one it was told about. Its Decorator shows a Component interface, a ConcreteComponent, and a Decorator abstract class holding a reference to what it wraps. Middleware.handle(Request, Chain) already captures both ideas in one functional interface, so most links in practice end up as lambdas or small final classes, not a class hierarchy under a shared abstract parent. java.io.BufferedInputStream wrapping a FileInputStream is the same Decorator idea living in the standard library: a type implementing the exact interface it wraps, adding one behaviour around a delegated call.
Ordering matters here more than either pattern's original write-up stresses. AdminGate has to run before anything that would log a rejected request, so the order links are registered in is part of the design, not an incidental detail left to whoever wires the list up last.
7. When naming it is wrong
A chain answers one question well: "should this also happen to every request passing through." Two links wired for exactly one caller, with no cross-cutting rule in sight, is not this pattern — it is a conditional wearing an interface, and the indirection buys nothing over calling the two methods directly.
The threshold is different from the earlier chapters' "is there a second implementation," because a chain can be worth building for its first two links if a third is clearly coming. Watch instead for the boundary where a chain answers badly. Once a rule depends on which route or method a request is, a single ordered list has nowhere to keep that decision, and every link ends up re-deriving routing information the router already had. On corpus/middleware-router, making one link's behaviour depend on the HTTP method costs 47 measured lines to retrofit, the most expensive change recorded anywhere in that corpus, against a design built around a router from the start. If a requirement reads "decide which steps apply, or in what order, based on something about this particular request," reach for a router instead of stretching the chain to cover it.
Your turn
Add a response-timing middleware: measure how long chain.next takes and record it, without editing AdminGate, AccessLog, or wrapping.
The answer.
final class ResponseTiming implements Middleware {
private final List<Long> callsTimed = new ArrayList<>();
@Override
public Response handle(Request request, Chain chain) {
long start = System.nanoTime();
Response response = chain.next(request);
callsTimed.add(System.nanoTime() - start);
return response;
}
int callsTimedCount() { return callsTimed.size(); }
}
Chain pipeline = wrapping(List.of(adminGate, timing, accessLog), terminal);
javac Step4.java
java Step4
log entries: 2
timed calls: 2
ResponseTiming has the same shape as AccessLog: it always calls chain.next and acts on the way back, so it is a Decorator too. Two requests were logged and two were timed, matching exactly, because timing sits between adminGate and accessLog and only ever sees what adminGate let through.
Going deeper
Decorator order is not cosmetic, and most treatments of this pattern do not say so plainly enough. Two links that each work correctly in isolation can produce a very different overall system depending on which one wraps which, and nothing about the code makes that obvious on a quick read. Add a retry link that calls chain.next up to three times when the response looks like a server error.
final class RetryMiddleware implements Middleware {
private static final int MAX_ATTEMPTS = 3;
@Override
public Response handle(Request request, Chain chain) {
Response response = null;
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
response = chain.next(request);
if (response.status() < 500) {
return response;
}
}
return response;
}
}
Wire it two different ways around a service that fails its first two calls and succeeds on the third.
// Case A: logging wraps retry — logging is outer, registered first.
Chain chainA = wrapping(List.of(loggingA, new RetryMiddleware()), serviceA::call);
// Case B: retry wraps logging — retry is outer, registered first.
Chain chainB = wrapping(List.of(new RetryMiddleware(), loggingB), serviceB::call);
javac Step5.java
java Step5
Case A (logging outside retry)
final status: 200
service calls: 3
log entries: 1
Case B (logging inside retry)
final status: 200
service calls: 3
log entries: 3
Both cases return the same final status, and both hit the flaky service three times. The log line count is not the same at all. One entry when logging sits outside the retry loop, because the caller only sees one overall attempt at the request. Three entries when logging sits inside it, because logging then runs once per retry attempt, including the two that failed. Neither count is wrong. One counts requests as the caller saw them; the other counts attempts as the service saw them, and nothing in either middleware's code says which count you are getting — only the order the two names appear in one List.of(...) call does. Write that order down as a comment or in a design doc, because a reader tracing a bug through this pipeline six months from now has no other way to know which one they are looking at.
Why this matters in an interview
Most candidates can produce a chain of handlers when asked directly. Fewer can say, unprompted, which of their links only decide and which ones also act on the response, and fewer still say out loud which one has to run first and why, before an interviewer asks "does the order matter here." That is the habit worth building now, while you can still run the code and watch the log count change.
Next: chapter 3.7, Template method: a fixed skeleton with holes. This chapter's chains vary in which links exist and what order they run in. The next one is the opposite shape: a sequence that never changes, with a small number of fixed points where one step is allowed to differ.
← 3.5 State: a transition table you can read · All chapters · 3.7 Template method: a fixed skeleton with holes →