Middleware Pipeline
HTTP middleware chain where each link processes the request, optionally short-circuits, or delegates to the next. Builder assembles the pipeline fluently. Template Method defines the middleware skeleton.
Key Abstractions
Immutable data classes carrying method, path, headers, and body through the pipeline
Abstract base class using Template Method — defines handle() skeleton with pre_process and post_process hooks
Concrete middleware implementations that override pre_process and post_process hooks
Wraps an HttpResponse to add headers or compress the body without modifying the original
Fluent builder that assembles middleware in explicit order via .use(auth).use(cors).use(compress).build()
Wraps a plain function or third-party handler to conform to the Middleware interface
Class Diagram
How It Works
A middleware pipeline is a chain of processors that sit between the raw HTTP request and your application logic. Each middleware gets the request, does some work, and either passes it to the next middleware or short-circuits the chain by returning a response immediately.
The Template Method pattern defines the skeleton. The abstract Middleware class has a handle() method that calls pre_process, then delegates to the next handler, then calls post_process. Concrete middleware only override the hooks they care about. AuthMiddleware overrides pre_process to check tokens. CompressionMiddleware overrides post_process to wrap the response body. Neither one touches the chain-linking logic.
Chain of Responsibility connects the middlewares. Each one holds a reference to the next handler. When pre_process returns an early response (auth failure), the chain stops. No downstream middleware runs. When it returns None, the request flows to the next link.
The Builder assembles the chain in a specific order. Calling .use(logging).use(auth).use(cors).use(compression) means logging runs first on the way in and last on the way out. The builder makes this ordering explicit and prevents accidental reordering.
Response Decorators wrap the response object without mutation. CompressedResponse wraps the original, replacing the body with a compressed version and adding a Content-Encoding header. HeaderInjectedResponse adds CORS headers. These decorators compose: you can compress a CORS-decorated response without either decorator knowing about the other.
Requirements
Functional
- Define a middleware interface with pre-processing and post-processing hooks
- Support short-circuiting: a middleware can reject a request and stop the chain
- Provide a fluent builder for assembling middleware in a specific order
- Support adapting plain functions or third-party handlers to the middleware interface
- Decorate responses (compression, header injection) without modifying the original object
Non-Functional
- Pipeline is built once at startup and reused for all requests
- Middleware order is deterministic and explicit
- Each middleware is a single-responsibility class, easy to add, remove, or reorder
- No shared mutable state between concurrent requests (frozen request objects)
Design Decisions
Couldn't you just loop over middleware in a list?
A for-loop iterating over middleware gives you pre-processing, but post-processing becomes awkward. You would need a second reverse loop after the handler runs. Chain of Responsibility gives you a natural call stack. pre_process runs on the way in, next.handle() recurses deeper, and post_process runs on the way out. It mirrors a normal function call and return. Short-circuiting is trivial: just return early without calling next.
Does middleware order really need a Builder?
Middleware order is not alphabetical; it's a deliberate design choice. Auth must run before business logic. Logging should wrap everything so you capture the full duration. Compression should be the innermost response transformation so it compresses the final body. A Builder with .use() calls makes this sequence readable and explicit. A config file or auto-discovery would hide the ordering and lead to subtle bugs.
What if every middleware just implemented a handle() method directly?
A pure interface forces every middleware to implement the full handle(request, next) logic, including the call to next. Forgetting to call next silently drops the request. Template Method puts the chain logic in the base class. Subclasses only override pre_process and post_process. The base class guarantees next gets called unless there's an explicit short-circuit.
How do you integrate third-party middleware that doesn't match your interface?
Third-party libraries and legacy code won't conform to your Middleware abstract class. A metrics library might expose a (req, callback) -> resp function. MiddlewareAdapter wraps that function so the pipeline treats it like any other middleware. You get integration without modifying the third-party code or breaking your pipeline's type safety.
Interview Follow-ups
- "How would you add async middleware?" Change the handler signature to return a
Future<HttpResponse>or useasync/await. The Template Method skeleton becomesawait pre_process,await next.handle,await post_process. The chain structure stays the same; only the execution model changes. - "How would you add error-handling middleware?" Insert an error-handling middleware early in the chain that wraps the
next.handle()call in a try/catch. If any downstream middleware or the application throws, the error handler catches it, logs it, and returns a 500 response. This is exactly how Express.js error middleware works. - "How would you measure per-middleware latency?" Wrap each middleware with a
MetricsMiddlewaredecorator that records the time before and after calling the inner middleware'shandle(). Emit the timing to a metrics system with the middleware's class name as a label. This gives you a flamechart-style breakdown of where time is spent in the pipeline. - "How does this compare to Express.js vs Spring interceptors?" Express.js uses a flat
(req, res, next)signature: middleware mutatesreqandresdirectly and callsnext()to continue. Spring'sHandlerInterceptorseparatespreHandle,postHandle, andafterCompletion: closer to Template Method but with three hooks instead of two. Both are variations of Chain of Responsibility; the difference is how much structure the framework imposes.
45-Minute LLD Playbook
Each phase is what the interviewer expects you to do and say. Concrete steps, not topic hints. Diagrams are what you would sketch on the board.
- 15 min
Clarify scope and the chain semantics
GoalLock the request-immutability rule, the short-circuit semantics, and the build-once-reuse rule. Then ask the interviewer whether they want a diagram or code-first.
Do & Say- ASK·1Open with the immutability question: HttpRequest is frozen. Middleware that wants to add a header returns a new HttpRequest from pre_process, not a mutation.
- SAY·2Justify the freeze: No shared mutable state between threads. The pipeline is safe under concurrent requests.
- SAY·3Lock the short-circuit semantics: pre_process returns (processed_request, optional early_response). If early_response is not None, downstream middleware and this middleware's post_process do not run.
- SAY·4Name the canonical short-circuit user: AuthMiddleware returns a 401 without ever hitting business logic.
- SAY·5Pin the ordering contract: Logging wraps everything for full duration. Auth runs early so rejections are cheap. Compression wraps the response innermost so it compresses the final body.
- SAY·6Defend explicit ordering over auto-discovery: The Builder makes the sequence explicit. A config file or auto-discovery would hide it.
- SAY·7State the build-once rule: Pipeline is assembled at startup and reused. Building per-request burns allocations and introduces ordering bugs.
- ASK·8Ask the process question: Do you want a class diagram first showing Middleware, the five patterns, and the response decorators? Or go straight to code? Either is fine.
Interviewer is grading: Frozen request is the first sentence about concurrency. Short-circuit is a deliberate return from pre_process, not an exception. Order is build-time, not runtime. You ask the diagram-vs-code question instead of just diving in.
- 25-10 min
Sketch the Template Method and the chain wiring
GoalLock the Middleware.handle() skeleton, the Builder's reverse-wrap, and the ResponseDecorator composition before coding.
Do & Say- WRITE·1Write Middleware.handle() in three lines: processed_request, early_response = pre_process(request). if early_response: return early_response. response = next_handler(processed_request); return post_process(response).
- SAY·2Sell Template Method: Chain wiring lives in the base class. Subclasses cannot forget to call next, the base class guarantees it unless they short-circuit.
- WRITE·3Write PipelineBuilder.build(): handler = final_handler. for mw in reversed(self._middlewares): handler = lambda req: mw.handle(req, handler). Return handler.
- SAY·4Defend the reverse: First .use() runs outermost. Builder absorbs wrapping order in one place, callers never write the lambda by hand.
- SAY·5Pin Decorator composition on response: CompressedResponse wraps HttpResponse and replaces the body. HeaderInjectedResponse wraps and adds headers. compress(addCorsHeaders(response)) composes both, neither decorator knows about the other.
- SAY·6Walk the canonical chain logging -> auth -> CORS -> compression -> timing -> applicationHandler: On request: log, auth, then handler. On unwind: timing, compression, CORS, log.
- SAY·7Trace an auth failure: Only log and timing posts fire. CORS and compression are skipped because the chain short-circuits at auth.
- SAY·8If a diagram was requested, draw three clusters: Middleware abstract at top with five concretes (Auth, Logging, Cors, Compression, MiddlewareAdapter). ResponseDecorator cluster off to the side. PipelineBuilder at the bottom with arrows up.
Interviewer is grading: Template Method skeleton in the base class. Builder reverses so .use() order is the run order. Response decorators compose. Auth short-circuit means CORS and compression do not run.
- 325 min
Code in this sequence (bottom-up)
GoalType code in the order HttpRequest / HttpResponse data classes, ResponseDecorator and two concretes, Middleware abstract with Template Method, four concrete middlewares, MiddlewareAdapter, PipelineBuilder, then demo. Talk through invariants while typing.
Do & Say- SAY·1Start with two data classes: HttpRequest: frozen dataclass with method, path, headers, body. HttpResponse: mutable dataclass with status_code, body, headers. Request is frozen so concurrent requests cannot stomp each other's headers. (~2 min)
- SAY·2Code ResponseDecorator as a subclass of HttpResponse: Constructor takes a wrapped HttpResponse. Copies status_code, body, and headers (dict copy, not reference). Subclasses then mutate their own copy. (~2 min)
- SAY·3Code CompressedResponse(wrapped) in three moves: Compress wrapped.body with gzip. Replace self.body with a marker like [gzip:42B from 200B]. Set self.headers Content-Encoding to gzip. (~2 min)
- SAY·4Code HeaderInjectedResponse(wrapped, extra_headers): Constructor copies via super. Then self.headers.update(extra_headers). (~1 min)
- SAY·5Code Middleware abstract base: handle(request, next_handler) is the Template Method. pre_process default returns (request, None). post_process default returns response. Subclasses override exactly one hook. (~3 min)
- SAY·6Code AuthMiddleware: Constructor takes valid_tokens. pre_process reads Authorization header, strips Bearer, checks membership. On miss return (request, HttpResponse(401, Unauthorized)). On hit return (request, None). (~3 min)
- SAY·7Code LoggingMiddleware: pre_process prints >>> {method} {path} and records self._start = time.time(). post_process prints <<< {status_code} ({elapsed}ms). Logging straddles both hooks. (~2 min)
- SAY·8Code CorsMiddleware: post_process wraps response in HeaderInjectedResponse with Allow-Origin, Allow-Methods, Allow-Headers. Decorator, not mutation: the wrapped response is unchanged. (~2 min)
- SAY·9Code CompressionMiddleware: post_process returns CompressedResponse(response). One line, innermost on response so it sees the final body. (~1 min)
- SAY·10Code MiddlewareAdapter: Constructor takes a (request, next) -> response callable and a name. Override handle directly to bypass Template Method and delegate to the wrapped function. Express-style middleware plugs in here. (~2 min)
- SAY·11Code PipelineBuilder: _middlewares list, use(mw) appends and returns self. build(final_handler) loops reversed and wraps via a closure. handler = lambda req: mw.handle(req, current_handler). (~3 min)
- SAY·12Walk four scenarios as a self-check: Valid token GET -> 200 with CORS, gzip, X-Timing. Invalid token -> 401, only log and timing fire. No Authorization header -> 401. Valid DELETE /api/sessions/42 -> 200 with full chain. (~1 min)
Interviewer is grading: HttpRequest is frozen. Template Method handle() is in the base class; subclasses override hooks only. Response decoration is non-mutating. Builder reverses so .use() order is run order. Auth short-circuit prevents CORS and compression from running.
- 45 min
Trade-offs and extensions
GoalDefend two trade-offs, volunteer one extension, close in one sentence.
Do & Say- SAY·1Trade-off one, Chain of Responsibility with a recursive call stack over a for-loop: A for-loop handles pre-processing but needs a second reverse loop for post. Chain gives post-processing for free via the call stack. Short-circuit is just an early return.
- SAY·2Trade-off two, Template Method over a pure interface: A pure handle(request, next) interface forces every subclass to remember to call next. Template Method moves chain wiring into the base class and exposes only pre_process and post_process hooks.
- SAY·3Volunteer error-handling middleware: Insert ErrorHandlingMiddleware early. Its handle wraps next.handle in try/catch. Any downstream exception becomes a 500, logged with full context. Express.js error middleware works exactly this way.
- WATCH·4Be ready for async: Change handler signature to return Future<HttpResponse> or use async/await. handle becomes await pre_process, await next, await post_process. Chain shape is identical, only the execution model changes.
- WATCH·5Be ready for per-middleware metrics: Wrap each middleware in a MetricsMiddleware decorator that times the inner handle. Emit timings with class name as label. Flamechart with no code edits.
- SAY·6Close in one sentence: Chain of Responsibility with recursion. Template Method to enforce next-is-called. Builder for explicit order. Decorator on responses. Adapter for third-party handlers. Frozen request for concurrency safety.
Interviewer is grading: You defend Chain over for-loop with the post-processing argument. You name Template Method as the bug-prevention mechanism for forgotten next calls. You volunteer error-handling middleware as a natural insertion point. You can summarize the design in one breath.
Code Implementation
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Callable, Optional
import gzip
import time
# ─── Data Classes ───────────────────────────────────────────────
@dataclass(frozen=True)
class HttpRequest:
method: str
path: str
headers: dict = field(default_factory=dict)
body: str = ""
@dataclass
class HttpResponse:
status_code: int
body: str
headers: dict = field(default_factory=dict)
# ─── Handler Type ───────────────────────────────────────────────
Handler = Callable[[HttpRequest], HttpResponse]
# ─── Response Decorators (Decorator Pattern) ────────────────────
class ResponseDecorator(HttpResponse):
"""Base decorator that wraps an existing HttpResponse."""
def __init__(self, wrapped: HttpResponse):
self._wrapped = wrapped
self.status_code = wrapped.status_code
self.body = wrapped.body
self.headers = dict(wrapped.headers)
class CompressedResponse(ResponseDecorator):
"""Simulates gzip compression on the response body."""
def __init__(self, wrapped: HttpResponse):
super().__init__(wrapped)
original_len = len(self.body)
compressed = gzip.compress(self.body.encode())
self.body = f"[gzip:{len(compressed)}B from {original_len}B]"
self.headers["Content-Encoding"] = "gzip"
class HeaderInjectedResponse(ResponseDecorator):
"""Injects extra headers into the response."""
def __init__(self, wrapped: HttpResponse, extra_headers: dict):
super().__init__(wrapped)
self.headers.update(extra_headers)
# ─── Middleware ABC : Template Method Pattern ───────────────────
class Middleware(ABC):
"""
Template Method: handle() defines the skeleton.
Subclasses override pre_process / post_process hooks only.
"""
def handle(self, request: HttpRequest, next_handler: Handler) -> HttpResponse:
# Step 1: pre-process : may short-circuit by returning a response
processed_request, early_response = self.pre_process(request)
if early_response is not None:
return early_response
# Step 2: delegate to next middleware in chain
response = next_handler(processed_request)
# Step 3: post-process the response
return self.post_process(response)
def pre_process(
self, request: HttpRequest
) -> tuple[HttpRequest, Optional[HttpResponse]]:
"""Override to inspect/modify request or short-circuit."""
return request, None
def post_process(self, response: HttpResponse) -> HttpResponse:
"""Override to inspect/modify response."""
return response
# ─── Concrete Middleware ────────────────────────────────────────
class AuthMiddleware(Middleware):
"""Checks for a valid Bearer token. Short-circuits on failure."""
def __init__(self, valid_tokens: set[str] | None = None):
self.valid_tokens = valid_tokens or {"token-abc", "token-xyz"}
def pre_process(self, request):
auth = request.headers.get("Authorization", "")
token = auth.replace("Bearer ", "") if auth.startswith("Bearer ") else ""
if token not in self.valid_tokens:
print(f" [AUTH] REJECTED : invalid token '{token}'")
return request, HttpResponse(401, "Unauthorized")
print(f" [AUTH] OK : token '{token}' accepted")
return request, None
class LoggingMiddleware(Middleware):
"""Logs incoming request and outgoing response."""
def pre_process(self, request):
print(f" [LOG] >>> {request.method} {request.path}")
self._start = time.time()
return request, None
def post_process(self, response):
elapsed_ms = (time.time() - self._start) * 1000
print(f" [LOG] <<< {response.status_code} ({elapsed_ms:.1f}ms)")
return response
class CorsMiddleware(Middleware):
"""Adds CORS headers to every response."""
def __init__(self, allowed_origins: str = "*"):
self.allowed_origins = allowed_origins
def post_process(self, response):
decorated = HeaderInjectedResponse(response, {
"Access-Control-Allow-Origin": self.allowed_origins,
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
"Access-Control-Allow-Headers": "Authorization, Content-Type",
})
print(f" [CORS] Headers injected: Allow-Origin={self.allowed_origins}")
return decorated
class CompressionMiddleware(Middleware):
"""Wraps response body with gzip compression decorator."""
def post_process(self, response):
decorated = CompressedResponse(response)
print(f" [COMPRESS] {decorated.body}")
return decorated
# ─── Middleware Adapter (Adapter Pattern) ───────────────────────
class MiddlewareAdapter(Middleware):
"""
Wraps a plain function (req, next) -> resp to conform
to the Middleware interface. Useful for third-party or
legacy handlers.
"""
def __init__(self, fn: Callable[[HttpRequest, Handler], HttpResponse], name: str = "adapter"):
self._fn = fn
self._name = name
def handle(self, request: HttpRequest, next_handler: Handler) -> HttpResponse:
print(f" [ADAPTER:{self._name}] delegating to wrapped function")
return self._fn(request, next_handler)
# ─── Pipeline Builder (Builder Pattern) ─────────────────────────
class PipelineBuilder:
"""Fluent builder that assembles middleware into a handler chain."""
def __init__(self):
self._middlewares: list[Middleware] = []
def use(self, middleware: Middleware) -> PipelineBuilder:
self._middlewares.append(middleware)
return self
def build(self, final_handler: Handler) -> Handler:
"""
Wraps final_handler with middleware in reverse order so that
the first .use() call runs first in the chain.
"""
handler = final_handler
for mw in reversed(self._middlewares):
handler = self._wrap(mw, handler)
return handler
@staticmethod
def _wrap(mw: Middleware, next_handler: Handler) -> Handler:
def wrapped(req: HttpRequest) -> HttpResponse:
return mw.handle(req, next_handler)
return wrapped
# ─── Application Handler (simulates actual endpoint) ────────────
def application_handler(request: HttpRequest) -> HttpResponse:
"""Simulates a real endpoint returning JSON."""
body = f'{{"message": "Hello from {request.path}", "method": "{request.method}"}}'
return HttpResponse(200, body, {"Content-Type": "application/json"})
# ─── Demo ───────────────────────────────────────────────────────
if __name__ == "__main__":
# A simple third-party timing function adapted to Middleware
def timing_fn(req: HttpRequest, nxt: Handler) -> HttpResponse:
start = time.time()
resp = nxt(req)
elapsed = (time.time() - start) * 1000
resp.headers["X-Timing-Ms"] = f"{elapsed:.2f}"
return resp
timing_adapter = MiddlewareAdapter(timing_fn, name="timing")
# Build the pipeline: logging -> auth -> CORS -> compression -> timing
pipeline = (
PipelineBuilder()
.use(LoggingMiddleware())
.use(AuthMiddleware(valid_tokens={"token-abc", "token-xyz"}))
.use(CorsMiddleware(allowed_origins="https://example.com"))
.use(CompressionMiddleware())
.use(timing_adapter)
.build(application_handler)
)
print("=" * 60)
print("TEST 1: Valid request : full chain executes")
print("=" * 60)
req1 = HttpRequest(
method="GET",
path="/api/users",
headers={"Authorization": "Bearer token-abc"},
)
resp1 = pipeline(req1)
print(f" Status : {resp1.status_code}")
print(f" Body : {resp1.body}")
print(f" Headers: {resp1.headers}\n")
print("=" * 60)
print("TEST 2: Invalid token : auth short-circuits the chain")
print("=" * 60)
req2 = HttpRequest(
method="POST",
path="/api/orders",
headers={"Authorization": "Bearer bad-token"},
)
resp2 = pipeline(req2)
print(f" Status : {resp2.status_code}")
print(f" Body : {resp2.body}")
print(f" Headers: {resp2.headers}\n")
print("=" * 60)
print("TEST 3: Missing Authorization header : auth short-circuits")
print("=" * 60)
req3 = HttpRequest(method="GET", path="/api/health")
resp3 = pipeline(req3)
print(f" Status : {resp3.status_code}")
print(f" Body : {resp3.body}")
print(f" Headers: {resp3.headers}\n")
print("=" * 60)
print("TEST 4: Another valid request to different path")
print("=" * 60)
req4 = HttpRequest(
method="DELETE",
path="/api/sessions/42",
headers={"Authorization": "Bearer token-xyz"},
)
resp4 = pipeline(req4)
print(f" Status : {resp4.status_code}")
print(f" Body : {resp4.body}")
print(f" Headers: {resp4.headers}\n")
print("Pipeline built once, reused for all four requests.")
print("Notice auth short-circuit skips CORS, compression, and timing.")Interview Grading by Level
What an interviewer at each level expects to see in your answer. Use this to calibrate, not to perform.
Junior Engineer (L3)
Recognizes that middleware is a chain and writes a basic Middleware interface plus auth and logging, but mutates the request, forgets to call next, or hardcodes the build order.
- Names Middleware as the unit and writes a handle method that takes a request and returns a response.
- Implements AuthMiddleware with a token check and a 401 response.
- Implements LoggingMiddleware that prints the request line.
- Recognizes that some middleware needs to run before business logic and some after.
- Adds a builder when prompted, even if it just appends to a list.
- Mutates the shared request object in middleware, creating race conditions under concurrent traffic.
- Forgets to call next in some middleware, silently dropping the request.
- Implements compression by mutating the response body instead of wrapping it.
- Hardcodes the middleware order inside the constructor instead of via a Builder.
- Builds the pipeline per-request inside the handler.
Mid-Level Engineer (L4)
Drives the design with Template Method on Middleware, Builder for ordering, response decorators, and an Adapter for third-party handlers.
- Writes Middleware.handle() in the base class with pre_process and post_process hooks; concrete subclasses override only the hooks.
- Implements PipelineBuilder.build() with the reverse-wrap loop so .use() order is the run order.
- Uses HttpRequest as frozen and HttpResponse via decoration (CompressedResponse, HeaderInjectedResponse) instead of mutation.
- Implements short-circuit by returning an early response from pre_process; auth failure skips CORS and compression.
- Adds MiddlewareAdapter to wrap a plain (req, next) -> resp function as a Middleware.
- Calls out that pipeline is built once at startup and reused for every request.
- Does not volunteer error-handling middleware as the natural extension.
- Misses the async story until prompted.
- Treats Template Method as a stylistic choice instead of as the bug-prevention mechanism for forgotten next calls.
Senior Engineer (L5+)
Volunteers error handling, async, and per-middleware metrics before being asked, names every invariant, and frames each pattern around a specific failure mode.
- Volunteers an ErrorHandlingMiddleware early in the chain that wraps downstream in try/catch and converts to 500, unprompted.
- Volunteers the async variant with await on pre_process, next, and post_process and explains that the chain shape is unchanged.
- Volunteers a MetricsMiddleware decorator that times the inner middleware and emits per-class labels.
- Names frozen-request as the invariant that makes concurrent processing safe.
- Frames each pattern around its failure mode: Chain of Responsibility for natural pre+post-processing via the call stack, Template Method for forgotten-next-call prevention, Builder for explicit ordering, Decorator for non-mutating response composition, Adapter for third-party plug-in without forking.
- Compares the design to Express.js (flat (req, res, next), middleware mutates) and Spring HandlerInterceptor (preHandle, postHandle, afterCompletion, closer to Template Method).
- Suggests a per-route pipeline variant where the Builder produces multiple pre-built handlers keyed by route prefix, so different endpoints get different middleware stacks.
- Closes with a one-sentence summary that names Chain, Template Method, Builder, Decorator, and Adapter and what each one prevents in under 20 seconds.
Common Mistakes
- ✗Wrong middleware order: compression before auth means you compress error responses too, wasting CPU on 401s.
- ✗Not calling next in the chain: silently drops the request and returns None or an empty response.
- ✗Mutating the shared request object: race conditions with concurrent requests when headers or body are modified in place.
- ✗Building the pipeline at runtime on every request instead of once at startup: unnecessary allocation and ordering bugs.
Key Points
- ✓Chain of Responsibility: each middleware decides to process and forward, or short-circuit (e.g., auth rejects with 401 and stops the chain).
- ✓Builder: pipeline construction order matters. Auth before logging before compression. Builder makes this explicit and fluent.
- ✓Template Method: Middleware base class defines handle() skeleton: pre_process, then next.handle, then post_process. Subclasses only override hooks.
- ✓Decorator on responses: compression and header injection are response wrappers that compose without altering the original response object.