LLD Dojo

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:


1. The situation

Your 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

One behaviour, wrapped around one call. You can read it end to end. 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 must 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 reached the timing or the logging code, exactly as required.

The method still works. Now notice what it has become. Three unrelated concerns — security, timing, logging — interleaved in one block. Turn any one of them off and you are 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 like 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 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.

Change how one of them decides to act and you risk breaking a check that has nothing to do with it, because they share the same lines instead of living in their own.

5. The move

Look at both rules from the outside and they turn out to be the same shape. Something that receives a request plus "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), then 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 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 for you. An ordinary one, a rejected admin request, and an authorised one.

Notice the rejected one never appears in your log. AdminGate runs first and never calls chain.next for it, so AccessLog never sees that request at all.

Now here is the part worth slowing down for. Those 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, then does something with the response that comes back. That is Decorator — a wrapper adding behaviour before or after a call it does not own, without changing the call's shape.

One question 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 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) captures both ideas in one functional interface. So most links in practice end up as lambdas or small final classes, with no class hierarchy under a shared abstract parent.

You have already used this without naming it. java.io.BufferedInputStream wrapping a FileInputStream is the same Decorator idea in the standard library: a type implementing the exact interface it wraps, adding one behaviour around a delegated call.

One thing matters here more than either pattern's original write-up stresses. Ordering. AdminGate has to run before anything that would log a rejected request, so the order you register links in is part of the design, and 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 a conditional wearing an interface. The indirection buys you nothing over calling the two methods directly.

Your threshold here differs 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.

The measurement is on record. 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.

So when 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

Your ResponseTiming has the same shape as AccessLog. It always calls chain.next and acts on the way back, so you have written a Decorator too.

Two requests logged, two 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 on their own can produce a very different overall system depending on which wraps which. Nothing in 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 to you 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;
    }
}

Now wire it two 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

Read those two blocks side by side.

Both cases return the same final status. 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 ever saw one overall attempt. Three entries when logging sits inside it, because logging then runs once per 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 here is what should worry you: 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.

So write that order down, in a comment or a design doc. Somebody 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 also act on the response.

Fewer still say out loud which one has to run first, and why, before an interviewer asks "does the order matter here?"

Build that habit 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 →