skip to content

HTTP middleware/filter pipelines are usually described as Chain of Responsibility, but a middleware wraps the rest of the chain and sees the response on the way back. How does that variant differ from the classic pattern, and when does the difference matter?

level: seniorimportance: should knowfreq 35%

answer

  1. next as a parameter = bracket the rest
  2. pre-logic forward, post-logic reverse order
  3. short-circuit = don't call next
  4. structurally Decorator, intent CoR
  5. async: must return or await next()

basics

~20 s

Classic Chain of Responsibility passes the request forward until one handler takes it. Middleware instead receives a next function, so it runs code before and after the rest of the chain — enabling timing, error wrapping, and response rewriting on the way back.

solid answer

~60 s

The classic chain is **one-way and terminal**: a handler either consumes the request or delegates, and control does not meaningfully return. The middleware/filter variant makes continuation an explicit call — `handle(request, next)` — so each stage brackets the remainder of the chain: it can run pre-logic, call `next`, then run post-logic on the result. That gives you timing and metrics, exception translation via try/catch around `next`, response header injection, transaction and context scoping, and short-circuiting by simply not calling `next` (auth rejection, cache hit). Structurally this is closer to nested Decorators than to a flat linked list; the sequence is a call stack, not a loop. It matters when a stage must observe the outcome, must restore state (close a scope, unbind a thread-local, end a span), or when the flow is asynchronous — with futures or coroutines the stage must return/await `next`'s result or the post-logic runs at the wrong time. Cost: deeper stacks, harder debugging, and the risk of a middleware forgetting to call `next` and silently truncating the pipeline.

code

pseudocode · 12 lines
pseudocode
// middleware brackets the remainder of the pipeline
fun timing(req, next): Response {
  val t0 = now()
  try { return next(req) }                 // must RETURN/await in async runtimes
  finally { metrics.record(now() - t0) }   // post-logic runs in REVERSE order
}

fun auth(req, next): Response =
  if (req.principal == null) Response.unauthorized()  // short-circuit: next not called
  else next(req)

val pipeline = timing wraps auth wraps dispatcher   // dispatcher = classic first-match chain

go deeper

for a junior

Know that middleware receives a 'next' function it can call to continue, and that skipping the call stops the request from going further.

for a middle

Explain the bracket: pre-logic, call next, post-logic — and give uses such as timing, error mapping, and adding response headers, plus short-circuiting for auth or a cache hit.

for a senior

Contrast intent (CoR) with structure (Decorator), cover reverse-order unwinding, async return/await correctness, context scoping and cleanup in finally, and the truncation bug from a forgotten next.

for a principal

Govern the pipeline as a platform contract: define phases and what each may assume, cap the per-request tax by budgeting stages, mandate that context propagation and span/transaction scoping live in framework-owned stages, and combine a cross-cutting pipeline terminating in an exhaustive dispatcher rather than one 30-stage chain.

## Two shapes of the same family **Classic Chain of Responsibility (GoF).** Handlers form a list. Each one asks "is this mine?" — if yes it processes and stops; if no it hands the request to the successor. Control conceptually flows forward only; the interesting event is *who consumed it*. **Middleware / filter / interceptor pipeline.** Each stage is invoked as `handle(request, next)` where `next` is a callable representing *the entire rest of the pipeline*. The stage may: - do work **before** calling `next` (authenticate, validate, start a timer, open a span, bind a context) - **call** `next` — or not, which short-circuits everything downstream - do work **after** `next` returns (record latency, add response headers, translate exceptions, commit/roll back, close the scope) This is the shape of servlet filters, ASP.NET Core middleware, Express/Koa middleware, gRPC interceptors, Rack, and most API gateways. ## The structural difference | | Classic chain | Middleware pipeline | |---|---|---| | Continuation | implicit (`successor.handle`) or driven by a loop | explicit `next` parameter the stage invokes | | Control flow | forward, terminal at first taker | nested call stack: forward *and* unwind | | Sees the response? | no | yes, on the way back | | Every stage runs? | usually not (first match wins) | usually yes, unless a stage short-circuits | | Closest sibling pattern | dispatch/first-match | Decorator (each layer wraps the core) | Because continuation is a call rather than a return value, the pipeline is really a set of nested closures assembled once (`f1(f2(f3(core)))`) — the same wrapping structure as Decorator, with the added right to *not* call the wrapped thing. ## What the two-way flow buys you - **Timing / metrics** — start a clock, `next`, stop the clock. Impossible in a one-way chain without every handler cooperating. - **Exception translation & mapping** — `try { next() } catch (e) { map to a 500/problem-details response }`. One stage protects everything downstream. - **Resource and context scoping** — open a transaction, DB session, tenant context, tracing span, MDC/thread-local; `next`; then close in a `finally`. The bracket guarantees cleanup on both success and failure paths. - **Response mutation** — compression, `ETag`, security headers, redaction, envelope wrapping. - **Caching** — check the cache; on a hit return without calling `next`; on a miss call `next` and store the result on the way back. One stage, both directions. - **Retry / circuit breaking** — call `next` more than once, or refuse to. ## Costs and hazards - **Forgetting to call `next`** silently truncates the pipeline — the request "disappears" or returns an empty response. Symptom: no error, blank 200. Guard with a test asserting the terminal handler ran, and a per-stage counter. - **Calling `next` twice** by accident (branching code paths) double-executes the core; make the continuation single-use or idempotent where that matters. - **Deep stacks** — with 20 stages, every stack trace has 20 frames of framework noise, and stack-depth limits matter in recursive/async runtimes. - **Async correctness** — if `next` returns a future/promise, the stage must **return or await** it; forgetting means post-logic executes before the downstream work completes, spans close early, transactions commit prematurely, and exceptions surface as unhandled rejections. Context propagation across threads (thread-locals) breaks unless the framework carries it. - **Ordering is doubly load-bearing** — a stage's pre-logic runs in registration order while its post-logic runs in **reverse** order. Putting compression before or after redaction, or the error mapper inside rather than outside the auth stage, changes behavior in ways that are easy to get wrong. Draw the bracket structure, not the list. - **Blurred responsibility** — because every stage runs, the pipeline drifts into a dumping ground for cross-cutting concerns; without a phase discipline it becomes a 30-stage tax on every request. ## Choosing between them Use the **classic first-match chain** when the question is *who is responsible for this request* — dispatchers, escalation tiers, parser selection, approval levels — and nothing needs to observe the outcome. Use the **middleware pipeline** when stages are *cross-cutting* — they apply to (nearly) every request, must bracket downstream work, or must transform the response. A common real design uses both: a middleware pipeline for cross-cutting concerns that terminates in a dispatcher, which is itself a first-match chain or a lookup map over concrete handlers. ## Interview framing Saying "middleware is just Chain of Responsibility" is defensible but incomplete. The precise statement: middleware shares CoR's intent of decoupling the sender from receivers and allowing a stage to stop propagation, but adds Decorator-style wrapping so each stage brackets the remainder — which is what makes timing, cleanup, and response rewriting possible.

  • A middleware records a latency metric after calling next, but in an async framework the numbers are implausibly small. What went wrong?
    The stage called next without returning or awaiting the future it produced. The post-logic ran as soon as the downstream work was *scheduled*, not when it completed, so the timer stopped almost immediately. The same bug closes tracing spans early, commits transactions before the work finishes, and turns downstream exceptions into unhandled rejections. Fix: return or await next's result and put post-logic in the completion callback or a finally on the awaited call.
  • In what order do post-processing steps run relative to registration order?
    Reverse. If stages are registered A, B, C, the pre-logic runs A, B, C and the post-logic unwinds C, B, A — it is a call stack, not a queue. That is why an error-mapping or response-compression stage is registered early (outermost): it must wrap everything downstream to see all failures and the final body.

A relay of inspectors along a conveyor: a classic chain is each inspector either taking the box off the line or waving it on. Middleware is each inspector stamping the box, following it through the rest of the line, and stamping it again on the way out — so they can measure how long the trip took and fix the label before it leaves.

saying these in an interview costs you the question

  • Claiming middleware is identical to classic Chain of Responsibility with no qualification.
  • Assuming post-processing runs in registration order rather than reverse.
  • Forgetting to call next, which silently truncates the pipeline with no error.
  • In async runtimes, calling next without returning or awaiting it, so timers, spans, and transactions close early.
  • Piling every cross-cutting concern into the pipeline until each request pays a 30-stage tax.

context