When building REST APIs and web applications with Spring Boot, request interception is often necessary to handle authentication, logging, security, request transformation, or other cross-cutting concerns. Spring Boot provides two powerful mechanisms for this: Filters and Interceptors.
While both allow you to modify requests before they reach controllers or responses before they are sent back, they operate at different levels of the application stack and serve different use cases.
This article will give you a deep dive into these two mechanisms, explaining when to use each, how they differ, and providing best practices to ensure a clean and maintainable architecture.
Watch the Video Tutorial
Want a step-by-step walkthrough? Watch my YouTube video where I explain everything in detail!
1. Understanding Filters in Spring Boot
What Are Filters?
Filters are part of the Servlet API and operate at a low level in the request lifecycle. They are executed before the request reaches the Spring MVC framework and can modify both the request and response.
Use Cases for Filters
- Logging and monitoring request details
- Authentication and security checks (e.g., JWT token validation)
- GZIP compression or request transformation
- CORS (Cross-Origin Resource Sharing) handling
- Request rejection based on specific conditions
GitHub Repository The full source code for this example is available on GitHub. Spring Boot Filters vs. Interceptors – GitHub Repository Feel free to explore, clone, and modify the code for your own learning!
Implementing a Filter in Spring Boot
With Spring Boot 3, the javax.servlet
package has been replaced with jakarta.servlet
due to the migration to Jakarta EE 10. Ensure you’re using the correct imports.
A filter must implement the jakarta.servlet.Filter
interface and override the doFilter
method.
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
@Component
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
System.out.println("> Filter: " + new ObjectMapper().writeValueAsString(Map.of(
"method", req.getMethod(),
"path", req.getRequestURI(),
"auth_header", Optional.ofNullable(req.getHeader(HttpHeaders.AUTHORIZATION)).orElse("")
)));
chain.doFilter(request, response);
}
}
By sending a request we end up with:
curl --location '<http://localhost:8080/orders>'
> Filter: {"method":"GET","auth_header":"","path":"/orders"}
Key Characteristics of Filters
- Defined at the Servlet level
- Applied to all requests before they reach controllers
- It is executed before reaching the Spring MVC
2. Understanding Interceptors in Spring Boot
What Are Interceptors?
Interceptors belong to the Spring MVC framework and operate at the Spring level. They are executed after the request has been mapped to a controller but before the controller method is executed.
Use Cases for Interceptors
- Modifying or adding request attributes before controller execution
- API rate limiting or request validation
- Custom authentication or authorization mechanisms
- Logging execution times of controller methods
- Enriching response data (e.g., adding custom headers)
Implementing an Interceptor in Spring Boot
To create an interceptor, implement the HandlerInterceptor
interface and override its methods.
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map;
@Component
public class RequestTimingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
request.setAttribute("startTime", System.currentTimeMillis());
return true; // Continue to the controller
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws JsonProcessingException {
Long startTime = (Long) request.getAttribute("startTime");
Long endTime = System.currentTimeMillis();
System.out.println("> Interceptor: " + new ObjectMapper().writeValueAsString(Map.of(
"start_time", format(startTime),
"end_time", format(endTime),
"duration_ms", System.currentTimeMillis() - startTime
)));
}
private String format(Long millis) {
return Instant.ofEpochMilli(millis)
.atZone(ZoneId.systemDefault())
.toLocalDateTime()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
}
}
The HandlerInterceptor
has 3 methods that can be implemented/override (in the previous example I implemented 2):
preHandle()
- Runs before the request reaches the controller.
- Used for authentication, logging, modifying the request, etc.
postHandle()
- Runs after the controller method executes, but before the response is returned.
- Used for modifying the response, adding extra attributes, etc.
afterCompletion()
- Runs after the complete request-response cycle (after the response is sent).
- Used for cleanup, logging, performance monitoring, etc.
Let’s take a look on this sequence diagram which represents the lifecycle of the Interceptor and it’s methods:

Registering an Interceptor
To activate an interceptor, you need to register it in a WebMvcConfigurer
bean.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private RequestTimingInterceptor requestTimingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestTimingInterceptor);
}
}
By sending a request we end up with:
curl --location '<http://localhost:8080/orders>'
> Interceptor: {"end_time":"2025-03-04 10:58:15.509","start_time":"2025-03-04 10:58:15.483","duration_ms":26}
Key Characteristics of Interceptors
- Operate before and after controller execution
- Can modify
ModelAndView
before rendering a response - Can be applied selectively using
WebMvcConfigurer
3. Key Differences Between Filters and Interceptors

4. When to Use Which?
- Use Filters when you need low-level modifications that apply to all requests, such as security headers, request logging, or compression.
- Use Interceptors when you need controller-level logic, such as modifying request attributes, enforcing API rate limits, or adding response metadata.
For a better visualization, the following flowchart shows exactly the moments when the request is filtered and intercepted.

5. Best Practices for Using Filters and Interceptors
- Keep filters lightweight: Avoid expensive computations in filters as they run for every request.
- Use dependency injection in interceptors: Unlike filters, interceptors have access to Spring beans.
- Do not mix concerns: Use filters for request transformation and interceptors for business logic validation.
- Leverage
OncePerRequestFilter
if your filter should execute only once per request.
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomOnceFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
System.out.println("Executing OncePerRequestFilter");
filterChain.doFilter(request, response);
}
}
Conclusion
Both Filters and Interceptors are essential for handling cross-cutting concerns in Spring Boot applications. By understanding their differences and best practices, you can design cleaner, more scalable applications.
- Use Filters for low-level request modifications.
- Use Interceptors for controller-level logic enforcement.
- Follow best practices to ensure performance and maintainability.
Mastering these concepts will help you build robust, high-performance Spring Boot applications.