skip to content

When would you choose a Filter over a HandlerInterceptor (and vice versa)? Walk through the full execution flow of a request through both.

level: seniorimportance: should knowfreq 40%

answer

  1. Need raw bytes / static / errors -> Filter
  2. Need handler annotations / model -> Interceptor
  3. Filters wrap interceptors wrap handler; never interleave
  4. Security = filter layer, runs before interceptors
  5. ContentCachingResponseWrapper -> must copyBodyToResponse

basics

~20 s

Use a Filter for low-level, framework-agnostic work that must wrap the whole request or touch raw request/response bytes (logging with body caching, compression, CORS, security). Use an interceptor when you need the matched Spring handler or the ModelAndView. Flow: filters wrap interceptors wrap the handler.

solid answer

~40 s

Choose a Filter when the concern is framework-agnostic or must operate on the raw request/response, or apply beyond Spring MVC: request/response body logging via ContentCachingRequest/ResponseWrapper, GZIP, CORS, rate limiting, security (Spring Security is a filter chain), request-ID/MDC setup, wrapping the response to modify bytes. Choose a HandlerInterceptor when you need Spring MVC context: the target HandlerMethod and its annotations, path-pattern scoping, model manipulation before view render, or per-handler metrics. Execution flow for a normal request: Filter1.doFilter (in) -> Filter2.doFilter (in) -> DispatcherServlet -> HandlerMapping resolves handler -> Interceptor.preHandle (in order) -> controller method -> Interceptor.postHandle (reverse) -> view rendered -> Interceptor.afterCompletion (reverse) -> back out through Filter2 -> Filter1. So filters strictly wrap interceptors, which strictly wrap the handler; the two layers never interleave.

code

java · 31 lines
java
// Interceptor that needs handler metadata — a good fit for the interceptor layer
public class RoleInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws IOException {
        if (handler instanceof HandlerMethod hm) {
            RequiresRole ann = hm.getMethodAnnotation(RequiresRole.class);
            if (ann != null && !currentUserHasRole(ann.value())) {
                res.sendError(HttpServletResponse.SC_FORBIDDEN); // must write response ourselves
                return false; // abort: handler + postHandle skipped
            }
        }
        return true;
    }
}

// Body logging needs raw bytes — a filter with caching wrappers, not an interceptor
public class BodyLoggingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        var wrappedReq = new ContentCachingRequestWrapper(req);
        var wrappedRes = new ContentCachingResponseWrapper(res);
        try {
            chain.doFilter(wrappedReq, wrappedRes);
        } finally {
            log.info("req body: {}", new String(wrappedReq.getContentAsByteArray()));
            log.info("res body: {}", new String(wrappedRes.getContentAsByteArray()));
            wrappedRes.copyBodyToResponse(); // MUST copy cached body back to the real response
        }
    }
}

go deeper

for a junior

Give the rule of thumb (Filter=low level/whole request; Interceptor=handler-aware) and that filters wrap interceptors.

for a middle

Provide concrete examples for each and sketch the nested execution flow.

for a senior

Explain the strict layering (no interleaving), the exception-path differences, and body-wrapping mechanics.

for a principal

Reasons about placing security/tracing in the filter layer, cross-cutting consistency, and when ResponseBodyAdvice beats a wrapper filter.

## Decision framework Ask: **does the concern need Spring MVC knowledge?** ### Reach for a Filter when… - You must touch the **raw** request or response — reading/replacing the body, GZIP, encoding, wrapping (`ContentCachingRequestWrapper`, `ContentCachingResponseWrapper`, `HttpServletResponseWrapper`). - The concern must apply to **everything**, not just Spring handlers — static resources, other servlets, unmapped 404s, error dispatches. - It's a **framework-agnostic / infrastructural** concern: security (Spring Security's whole model is filters), CORS (`CorsFilter`), request-ID/MDC/tracing setup, rate limiting, tenant resolution, character-encoding, compression. - You need to run **before Spring MVC even initializes** for the request. ### Reach for a HandlerInterceptor when… - You need the **matched handler** — e.g. read a custom annotation on the `HandlerMethod` to decide behavior (`hm.getMethodAnnotation(RequiresRole.class)`). - You want **path-pattern scoping** with `addPathPatterns`/`excludePathPatterns` (cleaner than URL-pattern registration). - You want to **contribute to the model** before a server-side view renders (Thymeleaf/JSP), e.g. common attributes. - You want **per-controller timing/metrics** tied to the resolved handler. - The work is inherently **Spring MVC** and doesn't need raw byte access. ### The overlap and the tie-breaker Many concerns (auth, logging) *can* be done either way. Tie-breakers: - Need to modify a JSON body? Filter (with response wrapper) or `ResponseBodyAdvice`, not `postHandle`. - Need the handler's annotations? Interceptor. - Must also cover non-MVC/static/error paths? Filter. ## Full execution flow (normal, successful request) ``` CLIENT |> Filter1.doFilter (pre-chain code) |> Filter2.doFilter (pre-chain code) |> DispatcherServlet.doDispatch - HandlerMapping selects the handler + interceptor chain |> InterceptorA.preHandle (registration order) |> InterceptorB.preHandle |> @Controller handler method executes <| InterceptorB.postHandle (reverse order, has ModelAndView) <| InterceptorA.postHandle - View rendered (or message converter writes @ResponseBody) <| InterceptorB.afterCompletion (reverse order) <| InterceptorA.afterCompletion <| (DispatcherServlet returns) <| Filter2.doFilter (post-chain code, response committing) <| Filter1.doFilter (post-chain code) CLIENT receives response ``` Key properties: - **Filters strictly wrap interceptors**, which strictly wrap the handler. The layers never interleave — you cannot put a filter *between* two interceptors. - `preHandle` runs in **registration order**; `postHandle`/`afterCompletion` unwind in **reverse**. - On the **filter** side too, code before `chain.doFilter` runs top-down and code after runs bottom-up (classic chain unwinding). ## Exception path differences - If a filter's downstream `chain.doFilter` throws, the exception propagates back up through the outer filters (they can catch it) — filters see exceptions that a `@ControllerAdvice` might otherwise translate, because advice runs inside the DispatcherServlet. - Interceptor `postHandle` is skipped on handler exceptions; `afterCompletion` still runs with the exception. - A filter can observe the **final HTTP status** after Spring's exception handling wrote the response; an interceptor's afterCompletion sees the exception object but the status may reflect the resolved error. ## Practical architectural note Because Spring Security lives in the filter layer, security decisions generally happen **before** any interceptor. Don't try to do primary authentication in an interceptor if a filter-based mechanism is available — the filter runs first and covers more surface.

  • Why can't you put a filter between two interceptors in the execution order?
    They live at different layers. Filters run in the servlet container chain that wraps the entire DispatcherServlet; interceptors run inside the DispatcherServlet after handler mapping. All filters complete their inbound pass before any interceptor runs, so ordering values only rank filters-among-filters and interceptors-among-interceptors, never across the boundary.
  • You used a ContentCachingResponseWrapper to log the response body. Users report empty responses. Why?
    The wrapper caches the body in a buffer instead of writing it straight to the client. You must call copyBodyToResponse() (typically in a finally block) to flush the cached bytes to the real response, otherwise the client gets an empty body.

saying these in an interview costs you the question

  • Claiming an interceptor can sit outside/before the filter chain
  • Doing primary authentication in a postHandle interceptor
  • Forgetting copyBodyToResponse with ContentCachingResponseWrapper
  • Saying filters and interceptors can interleave by tuning order values
  • Trying to rewrite a @ResponseBody JSON in postHandle

context