Spring Boot Request Lifecycle Deep Dive: Filters, Interceptors, Controller Advice & Execution Flow
What really happens before and after your controller in Spring Boot?
Introduction
While beginning with your Spring MVC application, have you given a thought about-
- Where should authentication logic go?
- Where should the request logging happen?
- How can request and response level filtering and modification happen.
Many developers start by putting everything inside controllers. Authentication. Logging. Validation. Metrics. Tracing. Suddenly, controllers become 500-line monsters doing everything except business logic.
The result? Messy architecture. Poor reusability. Difficult debugging. Code duplication everywhere.
A clean backend is not just about writing good APIs. It is about understanding where logic belongs in the request lifecycle. This is exactly where Spring Boot’s request/response processing layers become powerful.
In this article, we will go deep into:
- Request lifecycle in Spring Boot
- Interceptors & Controller Advice
- Execution sequence & Latency impact
- Body & header tampering
- Separation of concerns
- What to use where (Decision Matrix)
- Production-grade best practices
By the end of this article, you will understand exactly what happens before and after a request hits your controller.
Why Do We Even Need Filters?
Let us first understand the real problem. Imagine building APIs without filters or interceptors. A typical controller may start looking like this:
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id, HttpServletRequest request) {
log.info("Request received");
String token = request.getHeader("Authorization");
validateToken(token);
metrics.increment();
User user = userService.getUser(id);
return ResponseEntity.ok(user);
}
}At first glance, this looks fine. But now imagine 100 controllers. You suddenly have authentication logic repeated everywhere, logging in every API, metrics duplicated, and header parsing everywhere.
Your controller becomes responsible for Authentication, Logging, Metrics, Tracing, Validation, and Business Logic. That is bad design. A controller should focus only on business logic. Everything else is called a cross-cutting concern.
What Are Cross-Cutting Concerns?
Cross-cutting concerns are things required by multiple APIs but are not actual business logic. Examples include:
- Authentication: Checking JWT tokens.
- Request Logging: Logging incoming payloads.
- Metrics: Tracking API latency.
- Correlation IDs: Tracing requests across microservices.
- Security: Validating headers and access.
- Compression: Compressing response payloads.
Instead of repeating these concerns in every controller, Spring Boot provides request lifecycle hooks: Filters, Interceptors, and Controller Advice.
The Big Picture: Spring Boot Request Lifecycle
When a client sends a request (e.g., GET /users/1), it does not go directly to your controller. It passes through multiple architectural layers first.
As you can see, filters execute even before Spring understands which controller will handle the request. That detail is powerful.
What Are Filters in Spring Boot?
Filters are components that execute before and after an HTTP request reaches Spring MVC. Think of filters as gatekeepers of incoming HTTP traffic. They operate at the Servlet Layer (like Tomcat or Jetty), which means they run before the framework routes the request to a specific @GetMapping.
Filters only know about the HttpServletRequest, HttpServletResponse, and HTTP metadata. They do not know your controller logic.
Common Use Cases of Filters
- Request Logging: Tracking incoming endpoints.
- Authentication: JWT token validation.
- Correlation IDs: Injecting request IDs.
- Header Manipulation: Adding or modifying headers.
Understanding Filters Through Code
Imagine a request enters /api/users. Before the controller executes, a filter intercepts it:
@Component
public class LoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
log.info("Incoming Request: {} {}", request.getMethod(), request.getRequestURI());
// This line passes the request forward!
filterChain.doFilter(request, response);
long endTime = System.currentTimeMillis();
log.info("Request completed in {} ms", (endTime - startTime));
}
}
Notice filterChain.doFilter(request, response). Without this line, the request stops. Your API will never reach the controller.
Why
OncePerRequestFilter? Spring guarantees this filter executes exactly once per request. PlainFilterinterfaces can accidentally run multiple times during internal dispatches. In production systems, always preferOncePerRequestFilter.
Can Filters Modify the Request Body?
Short answer: Yes. But there is a big catch.
The HTTP request body behaves like a stream. When you call request.getInputStream(), the stream gets consumed. Meaning: You cannot read it again. If your filter reads the JSON body, the controller will later receive an empty body, resulting in mysterious bugs.
The Correct Way: Wrappers
Spring provides ContentCachingRequestWrapper and ContentCachingResponseWrapper. These wrappers cache the body payload so it can be read multiple times.
Should Filters Modify the Request Body?
Technically, yes. Architecturally, usually no.
Body mutation introduces hidden side effects, breaks API contracts, and makes debugging a nightmare. Filters should ideally observe, validate, and enrich — not mutate business payloads. Only modify bodies for exceptional infrastructure reasons like decryption, PII masking, sanitization, or compression.
Should Filters Modify Headers?
YES — This is one of their best use cases.
Get Aryash Jain’s stories in your inbox
Join Medium for free to get updates from this writer.
Unlike the body, headers are lightweight metadata. They are expected to be enriched.
- Headers = Metadata
- Body = Business Data
Filters are perfect for injecting X-Correlation-ID, parsing Authorization headers, checking Tenant-ID, or appending security headers like Cache-Control on the way out.
Separation of Concern: The Real Superpower
This is the biggest benefit of the request lifecycle.
Without Filters:
Controller
├── Authentication
├── Logging
├── Metrics
├── Validation
└── Business LogicWith Filters:
Filter → Authentication
Filter → Logging
Interceptor → ValidationController
└── Business LogicSuddenly, controllers become clean, focused, maintainable, and reusable. This is how scalable backend systems are designed. A clean architecture is not achieved by writing better controllers; it is achieved by putting logic in the correct layer.
Interceptors: How Are They Different?
Filters intercept the raw HTTP request. Interceptors operate at the Spring MVC layer.
Because Interceptors live inside the Spring Context, they know exactly which HandlerMethod (Controller class and method) is going to execute.
Interceptors provide three hooks:
preHandle(): Before the controller logic.postHandle(): After the controller executes, but before the view/response is fully rendered.afterCompletion(): After the entire request has finished (great for resource cleanup).
Best used for: Role-Based Access Control (RBAC), API permission checks, audit logging based on the specific controller method being called.
Controller Advice: The Global Response Shaper
While Filters and Interceptors process traffic before it reaches the core business logic, Controller Advice is responsible for what happens afterward, specifically concerning the response shape and exceptional outcomes.
Developers often litter controllers with try/catch blocks to handle common exceptions (like database constraints, validation errors, or resource-not-found issues), turning controllers into error-handling monsters.
This approach fails the “Separation of Concerns” principle. Business logic becomes coupled with error reporting, and defining a standard error format across the API becomes impossible.
The Solution: @RestControllerAdvice
A class annotated with @RestControllerAdvice allows you to define centralized logic for all or some controllers, most commonly used for global exception handling using @ExceptionHandler.
By moving exception handling here, your controller methods focus only on the happy path.
By using @RestControllerAdvice, You can pull exception handling completely out of your controllers. It executes after the controller throws an exception, intercepting the error and transforming it into a standardized JSON response.
Java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(UserNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(ex.getMessage()));
}
}Sequence of Execution
This is the exact sequence of events when a request enters a Spring Boot application.
- Request enters application
- Filter (Before): Servlet filter chain begins executing.
- Dispatcher Servlet: Spring takes over routing.
- Interceptor
preHandle(): Spring MVC interceptor runs. - Controller: Your business logic executes.
- Service Layer & DB: Core application logic.
- Controller Response: Controller returns data or throws an exception.
- Controller Advice: Catches any thrown exceptions and formats errors.
- Interceptor
postHandle(): Runs after successful controller execution. - Interceptor
afterCompletion(): Final cleanup after the response is ready. - Filter (After): Servlet filter chain unwinds.
- Response returned to Client.
Do Filters Add Latency?
Yes — but it is usually negligible.
Both Filters and Interceptors generally execute in microseconds. Controller Advice adds very little overhead.The biggest mistake is putting expensive business logic inside filters. Keep them focused on fast, in-memory metadata validation.
Production Best Practices
- Keep filters lightweight. They run on every single HTTP request. Milliseconds add up.
- Avoid business logic in filters. If it requires a database transaction, it likely belongs in the service layer.
- Do not mutate the request body unless absolutely required. It breaks transparency.
- Prefer header manipulation over body manipulation.
- Always use
OncePerRequestFilterinstead of the standardFilterinterface. - Use Correlation IDs. Inject them at the very first filter and log them everywhere.
IN The End, build not just to build it, but build to scale
A clean backend is not built by writing better controllers. It is built by knowing where logic belongs. Separation of concerns should be the priority.
Filters, Interceptors, and Controller Advice exist to separate infrastructure concerns from business logic. Once you understand the request lifecycle and respect the boundaries of HTTP metadata vs. business data, Spring Boot suddenly becomes much easier to design at scale.









