spring-boot
filter-order
java
web-development
middleware

Filter order in spring-boot

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

In Spring Boot, filter execution order is controlled by the @Order annotation or by calling setOrder() on a FilterRegistrationBean. Filters with lower order values run first. When no order is specified, the execution sequence is undefined, which leads to subtle bugs when filters depend on each other. This article explains both ordering mechanisms, covers the interaction with Spring Security's filter chain, and provides concrete patterns for common filter configurations.

How Filter Ordering Works

Spring Boot builds a FilterChain that wraps all registered servlet filters. Each incoming HTTP request passes through every filter in the chain, in order. Each filter calls chain.doFilter(request, response) to pass control to the next filter. After the downstream filters and the servlet complete, control returns back through the chain in reverse order.

text
Request  -->  Filter A (order=1)  -->  Filter B (order=2)  -->  Servlet
Response <--  Filter A            <--  Filter B            <--  Servlet

This means a filter can modify both the request (before calling chain.doFilter) and the response (after calling chain.doFilter).

Method 1: @Order Annotation

Apply @Order directly to a @Component filter class. Lower values execute first.

java
1import jakarta.servlet.Filter;
2import jakarta.servlet.FilterChain;
3import jakarta.servlet.ServletException;
4import jakarta.servlet.ServletRequest;
5import jakarta.servlet.ServletResponse;
6import org.springframework.core.annotation.Order;
7import org.springframework.stereotype.Component;
8import java.io.IOException;
9
10@Component
11@Order(1)
12public class RequestLoggingFilter implements Filter {
13    @Override
14    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
15            throws IOException, ServletException {
16        System.out.println("Logging: request received");
17        chain.doFilter(request, response);
18        System.out.println("Logging: response sent");
19    }
20}
java
1@Component
2@Order(2)
3public class AuthenticationFilter implements Filter {
4    @Override
5    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
6            throws IOException, ServletException {
7        // Authentication logic runs after logging
8        String token = ((HttpServletRequest) request).getHeader("Authorization");
9        if (token == null || !isValid(token)) {
10            ((HttpServletResponse) response).sendError(401, "Unauthorized");
11            return;  // Short-circuit: do not call chain.doFilter
12        }
13        chain.doFilter(request, response);
14    }
15}
java
1@Component
2@Order(3)
3public class RateLimitFilter implements Filter {
4    @Override
5    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
6            throws IOException, ServletException {
7        // Rate limiting runs after authentication
8        if (isRateLimited(request)) {
9            ((HttpServletResponse) response).sendError(429, "Too Many Requests");
10            return;
11        }
12        chain.doFilter(request, response);
13    }
14}

With this setup, every request hits logging first, then authentication, then rate limiting. If authentication fails, the rate limit filter never executes.

Method 2: FilterRegistrationBean

FilterRegistrationBean gives you more control. You can set the order, restrict the filter to specific URL patterns, and disable the filter without removing the class.

java
1import org.springframework.boot.web.servlet.FilterRegistrationBean;
2import org.springframework.context.annotation.Bean;
3import org.springframework.context.annotation.Configuration;
4
5@Configuration
6public class FilterConfig {
7
8    @Bean
9    public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
10        FilterRegistrationBean<RequestLoggingFilter> bean = new FilterRegistrationBean<>();
11        bean.setFilter(new RequestLoggingFilter());
12        bean.setOrder(1);
13        bean.addUrlPatterns("/*");
14        return bean;
15    }
16
17    @Bean
18    public FilterRegistrationBean<AuthenticationFilter> authFilter() {
19        FilterRegistrationBean<AuthenticationFilter> bean = new FilterRegistrationBean<>();
20        bean.setFilter(new AuthenticationFilter());
21        bean.setOrder(2);
22        bean.addUrlPatterns("/api/*");  // Only apply to API routes
23        return bean;
24    }
25
26    @Bean
27    public FilterRegistrationBean<RateLimitFilter> rateLimitFilter() {
28        FilterRegistrationBean<RateLimitFilter> bean = new FilterRegistrationBean<>();
29        bean.setFilter(new RateLimitFilter());
30        bean.setOrder(3);
31        bean.addUrlPatterns("/api/*");
32        return bean;
33    }
34}

When using FilterRegistrationBean, do not annotate the filter class with @Component. If you do, Spring registers the filter twice: once through component scanning and once through the registration bean, which causes duplicate execution.

Preventing Double Registration

This is one of the most common Spring Boot filter mistakes. If your filter is annotated with @Component and you also register it with a FilterRegistrationBean, it runs twice per request.

To prevent this, either:

  1. Remove @Component from the filter class and use only FilterRegistrationBean.
  2. Keep @Component but disable the automatic registration by setting enabled to false:
java
1@Bean
2public FilterRegistrationBean<MyFilter> disableAutoRegistration(MyFilter myFilter) {
3    FilterRegistrationBean<MyFilter> bean = new FilterRegistrationBean<>(myFilter);
4    bean.setEnabled(false);  // Prevents double registration
5    return bean;
6}

Comparison of Ordering Approaches

ApproachURL Pattern ControlConditional DisableOrder MechanismBest For
@Component + @OrderNo (applies to all URLs)NoAnnotation valueSimple global filters
FilterRegistrationBeanYes (addUrlPatterns)Yes (setEnabled)setOrder() methodRoute-specific or configurable filters
Ordered interfaceNoNogetOrder() methodFilters that need dynamic order values

Interaction with Spring Security

Spring Security registers its own FilterChainProxy as a servlet filter with a default order of -100 (defined by SecurityProperties.DEFAULT_FILTER_ORDER). This means Spring Security's filter chain runs before most custom filters.

If you need a filter to run before Spring Security (for example, a CORS filter or a request ID generator), set its order below -100:

java
1@Bean
2public FilterRegistrationBean<RequestIdFilter> requestIdFilter() {
3    FilterRegistrationBean<RequestIdFilter> bean = new FilterRegistrationBean<>();
4    bean.setFilter(new RequestIdFilter());
5    bean.setOrder(-200);  // Before Spring Security (-100)
6    return bean;
7}

The Spring Security filter chain itself contains an ordered sequence of internal filters:

text
1Order  Filter
2--------------------------
3  1    ChannelProcessingFilter
4  2    SecurityContextPersistenceFilter
5  3    ConcurrentSessionFilter
6  4    UsernamePasswordAuthenticationFilter
7  5    BasicAuthenticationFilter
8  6    ExceptionTranslationFilter
9  7    FilterSecurityInterceptor

You can insert custom filters at specific points in this internal chain using HttpSecurity:

java
1@Bean
2public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
3    http
4        .addFilterBefore(new CustomJwtFilter(), UsernamePasswordAuthenticationFilter.class)
5        .addFilterAfter(new AuditFilter(), FilterSecurityInterceptor.class)
6        .authorizeHttpRequests(auth -> auth
7            .requestMatchers("/public/**").permitAll()
8            .anyRequest().authenticated()
9        );
10    return http.build();
11}

Common Filter Ordering Patterns

text
1Order   Filter                Purpose
2-----   ------                -------
3-200    RequestIdFilter        Assign a unique ID to every request
4-100    Spring Security        Authentication and authorization
5  1     RequestLoggingFilter   Log request method, path, and timing
6  2     CompressionFilter      Compress responses (after all content is written)
7  3     CachingFilter          Set cache headers

Implementing a Request Timing Filter

java
1@Component
2@Order(1)
3public class TimingFilter implements Filter {
4
5    private static final Logger log = LoggerFactory.getLogger(TimingFilter.class);
6
7    @Override
8    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
9            throws IOException, ServletException {
10        long start = System.nanoTime();
11        try {
12            chain.doFilter(request, response);
13        } finally {
14            long duration = (System.nanoTime() - start) / 1_000_000;
15            HttpServletRequest req = (HttpServletRequest) request;
16            HttpServletResponse res = (HttpServletResponse) response;
17            log.info("{} {} {} {}ms", req.getMethod(), req.getRequestURI(),
18                     res.getStatus(), duration);
19        }
20    }
21}

This filter wraps chain.doFilter in a try/finally block so the timing is recorded even if a downstream filter or servlet throws an exception.

Debugging Filter Order

When filter order is not behaving as expected, enable Spring Boot's debug logging to see the registered filter chain:

properties
logging.level.org.springframework.boot.web.servlet=DEBUG
logging.level.org.springframework.security.web=DEBUG

You can also log the filter chain programmatically:

java
1@Bean
2public CommandLineRunner logFilters(ServletContext ctx) {
3    return args -> {
4        ctx.getFilterRegistrations().forEach((name, reg) -> {
5            System.out.println("Filter: " + name + " -> " + reg.getClassName());
6        });
7    };
8}

Common Pitfalls

  • Not specifying an order at all. When multiple filters have no @Order annotation, their execution sequence is determined by class loading order, which is undefined and can change between builds.
  • Annotating a filter with both @Component and registering it with FilterRegistrationBean, causing it to execute twice per request.
  • Placing a CORS filter after Spring Security. The CORS preflight request (OPTIONS) is rejected by security before the CORS filter can add the required headers, resulting in opaque browser errors.
  • Assuming @Order(1) means "first." If Spring Security runs at order -100, any filter with a positive order runs after security. Use negative values for filters that must precede security.
  • Calling chain.doFilter() after writing to the response. Once the response is committed (status and headers sent to the client), modifying headers in a downstream filter has no effect.
  • Using @Order on a @Bean method that returns a FilterRegistrationBean. The @Order annotation on the bean method controls Spring bean initialization order, not filter execution order. Use bean.setOrder() instead.

Summary

  • Filter execution order in Spring Boot is controlled by @Order annotations or FilterRegistrationBean.setOrder(). Lower values run first.
  • Use @Component + @Order for simple global filters. Use FilterRegistrationBean when you need URL pattern filtering or conditional registration.
  • Spring Security registers at order -100. Filters that must run before security need a lower order value.
  • Avoid double registration by not combining @Component with FilterRegistrationBean for the same filter.
  • Always specify an explicit order on every filter. Relying on undefined default ordering causes intermittent bugs that are difficult to diagnose.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.