Insights into Company
← Backend Concepts in Depth topics

Middleware, Filters & Interceptors

Middleware is code that wraps every request before and after the controller. This lesson shows the request pipeline, writing middleware in Laravel, filters and interceptors in Spring Boot, the React and Kafka equivalents, and real banking, e-commerce and payment middleware.

Running examples: request id logging, rate limiting transfers, idempotent payments, maintenance windows

1. The request pipeline

A web request does not go straight to a controller. It passes through a stack of layers, each of which can inspect or change the request, stop it with its own response, or let it continue. On the way back the same layers can change the response. The pattern is called a pipeline or "onion".

Middleware is the right place for cross-cutting concerns: rules that apply to many routes and have nothing to do with a particular business action. Examples: authentication, CSRF, rate limits, CORS, security headers, request ids, locale, maintenance mode.

Laravel request through middleware

  1. Global: TrustProxies, maintenance check, trim strings
  2. Group web: cookies, session, CSRF
  3. Route: auth, throttle, can:
  4. Controller
  5. Response back out through the same layers
What belongs where
ConcernMiddleware?Why
Is the user logged in?YesSame for every protected route
Max 5 transfers per minuteYes (throttle)Generic, keyed by user
Can this user cancel order 187?No: policyNeeds the specific record
Calculate GST on an orderNo: serviceBusiness logic
Add X-Request-Id to logs and responseYesApplies to every request

2. Laravel middleware in practice

Request id + timing middleware (observability for every request)

class AssignRequestId
{
    public function handle(Request $request, Closure $next): Response
    {
        $id = $request->header('X-Request-Id') ?: (string) Str::uuid();
        Log::withContext(['request_id' => $id, 'user_id' => $request->user()?->id]);   // every log line now carries it
        $started = microtime(true);

        $response = $next($request);                     // controller runs here

        $response->headers->set('X-Request-Id', $id);
        $response->headers->set('Server-Timing', 'app;dur=' . round((microtime(true) - $started) * 1000));
        return $response;
    }
}

Rate limiting money transfers per customer

// AppServiceProvider::boot()
RateLimiter::for('transfers', fn (Request $r) => [
    Limit::perMinute(5)->by($r->user()->id),
    Limit::perDay(50)->by($r->user()->id)->response(fn () => response()->json(['message' => 'Daily transfer attempts exceeded.'], 429)),
]);

Route::post('/transfers', [TransferController::class, 'store'])->middleware(['auth:sanctum', 'throttle:transfers']);

Payment idempotency: a retried request must not charge twice

class EnsureIdempotency
{
    public function handle(Request $request, Closure $next): Response
    {
        $key = $request->header('Idempotency-Key') ?? abort(400, 'Idempotency-Key header required');
        $cacheKey = "idem:{$request->user()->id}:{$key}";

        return Cache::lock($cacheKey . ':lock', 10)->block(5, function () use ($cacheKey, $request, $next) {
            if ($saved = Cache::get($cacheKey)) {
                return response($saved['body'], $saved['status'])->header('Idempotent-Replay', 'true');
            }
            $response = $next($request);
            Cache::put($cacheKey, ['body' => $response->getContent(), 'status' => $response->getStatusCode()], now()->addDay());
            return $response;
        });
    }
}

The lock matters: a mobile app on a bad network may fire the same request twice within milliseconds. Without the lock both copies pass the cache check before either has saved a result.

3. Spring Boot: filters, interceptors and AOP

Spring has three layers that play the role of middleware. Servlet filters wrap the whole request, including Spring Security. HandlerInterceptors run around controller methods and know which handler was chosen. AOP aspects (@Around) wrap any bean method, including services, which is how @Transactional and @Cacheable work.

A filter and an interceptor

@Component
@Order(1)
class RequestIdFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        String id = Optional.ofNullable(req.getHeader("X-Request-Id")).orElse(UUID.randomUUID().toString());
        MDC.put("requestId", id);                 // appears in every log line
        res.setHeader("X-Request-Id", id);
        try { chain.doFilter(req, res); } finally { MDC.clear(); }
    }
}

@Component
class MaintenanceInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws IOException {
        if (flags.isOff("upi") && req.getRequestURI().startsWith("/api/upi")) {
            res.sendError(503, "UPI is under maintenance until 02:00");
            return false;                         // stop: controller never runs
        }
        return true;
    }
}

4. The same idea in React and Kafka

React has no server middleware, but the pattern appears in two places. Route guards wrap pages (redirect to login if there is no user). HTTP interceptors (an axios interceptor or a fetch wrapper) add the token and request id to every call and handle 401 centrally.

Kafka clients have ProducerInterceptor and ConsumerInterceptor. Teams use them to stamp every message with a trace id and the producing service, or to count messages for monitoring, without touching each producer call.

axios interceptors

api.interceptors.request.use(config => {
  config.headers['X-Request-Id'] = crypto.randomUUID();
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});
api.interceptors.response.use(r => r, error => {
  if (error.response?.status === 401) window.location.assign('/login');
  return Promise.reject(error);
});

5. Interview questions

Middleware vs policy?

Middleware is generic and runs before the controller without knowing the specific record; a policy decides about one model instance (this order, this account).

What is terminable middleware?

A terminate() method that runs after the response is sent to the browser, used for metrics or logging that should not slow the user.

Filter vs interceptor in Spring?

Filters are servlet-level and run for every request, including static and security; interceptors run inside Spring MVC around a resolved handler.

Why does order matter?

Session must start before authentication and authentication before authorization; a rate limiter keyed by user must run after auth.