Why Skip Your Own Rate Limiter? Using Spring Boot’s Built‑in ConcurrencyThrottleInterceptor

The article explains how Spring Boot 3.5 provides the ConcurrencyThrottleInterceptor for limiting concurrent method calls, demonstrates basic configuration and execution, reveals that all intercepted methods share a single limit, and proposes two fixes—per‑pointcut advisors or a BeanPostProcessor with a custom @ConcurrencyLimit annotation—before recommending dedicated libraries such as Bucket4j or Resilience4j for business‑level throttling.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Why Skip Your Own Rate Limiter? Using Spring Boot’s Built‑in ConcurrencyThrottleInterceptor

1. Introduction

Spring Boot 3.5 provides ConcurrencyThrottleInterceptor, a MethodInterceptor that blocks method invocations when a configured concurrency limit is reached.

2. Basic usage and configuration

Define the interceptor as a bean, set setConcurrencyLimit(2), and combine it with an AspectJExpressionPointcut in a DefaultPointcutAdvisor:

@Configuration
public class ConcurrentConfig {
  @Bean
  ConcurrencyThrottleInterceptor throttleInterceptor() {
    ConcurrencyThrottleInterceptor interceptor = new ConcurrencyThrottleInterceptor();
    interceptor.setConcurrencyLimit(2);
    return interceptor;
  }

  @Bean
  DefaultPointcutAdvisor concurrentAdvisor() {
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression("execution(* com.pack..*.*(..))");
    advisor.setPointcut(pointcut);
    advisor.setAdvice(throttleInterceptor());
    return advisor;
  }
}

Service method query() sleeps 2 seconds and prints thread name and timestamp. A test creates five threads invoking query(). Output shows at most two threads run concurrently, matching the limit.

3. Problem: shared limit across methods

When multiple methods (e.g., query() and batch()) are intercepted by the same ConcurrencyThrottleInterceptor, they share a single concurrency counter. A test that runs both methods concurrently produces interleaved output, indicating competition for the same two‑slot limit.

4. Solution 1 – Separate advisors per pointcut

Create a distinct DefaultPointcutAdvisor for each method, each with its own ConcurrencyThrottleInterceptor and limit:

@Bean
DefaultPointcutAdvisor concurrentAdvisorQuery() {
  AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
  pc.setExpression("execution(* com.pack.concurrent.service.UserService.query(..))");
  ConcurrencyThrottleInterceptor advice = new ConcurrencyThrottleInterceptor();
  advice.setConcurrencyLimit(2);
  return new DefaultPointcutAdvisor(pc, advice);
}

@Bean
DefaultPointcutAdvisor concurrentAdvisorBatch() {
  AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
  pc.setExpression("execution(* com.pack.concurrent.service.UserService.batch(..))");
  ConcurrencyThrottleInterceptor advice = new ConcurrencyThrottleInterceptor();
  advice.setConcurrencyLimit(3);
  return new DefaultPointcutAdvisor(pc, advice);
}

Execution shows each method respecting its own limit, but configuring many advisors becomes cumbersome.

5. Solution 2 – BeanPostProcessor with custom annotation

Define annotation @ConcurrencyLimit to declare a limit on a method:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConcurrencyLimit {
  int value(); // concurrency limit
}

Implement a BeanPostProcessor that scans beans for methods annotated with @ConcurrencyLimit, creates a dedicated ConcurrencyThrottleInterceptor for each, and attaches it via a dynamically built Advisor:

@Component
public class ConcurrencyBeanPostProcessor implements BeanPostProcessor {
  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    Class<?> targetClass = AopUtils.getTargetClass(bean);
    Object result = bean;
    List<Method> methods = new ArrayList<>();
    ReflectionUtils.doWithLocalMethods(targetClass, method -> {
      if (method.getAnnotation(ConcurrencyLimit.class) != null) {
        methods.add(method);
      }
    });
    for (Method method : methods) {
      boolean isProxy = AopUtils.isAopProxy(result);
      if (isProxy && result instanceof Advised adv) {
        adv.addAdvisor(createAdvisor(method));
      } else {
        ProxyFactory factory = new ProxyFactory();
        factory.setTarget(bean);
        factory.addAdvisor(createAdvisor(method));
        result = factory.getProxy();
      }
    }
    return result;
  }

  private DefaultPointcutAdvisor createAdvisor(Method method) {
    ConcurrencyThrottleInterceptor interceptor = new ConcurrencyThrottleInterceptor();
    interceptor.setConcurrencyLimit(method.getAnnotation(ConcurrencyLimit.class).value());
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(interceptor);
    advisor.setPointcut(new Pointcut() {
      final String methodName = method.toString();
      public MethodMatcher getMethodMatcher() {
        return (m, cls) -> m.toString().equals(methodName);
      }
      public ClassFilter getClassFilter() { return ClassFilter.TRUE; }
    });
    return advisor;
  }
}

This eliminates the need to declare an advisor for every method, but requires writing the annotation and post‑processor, which the author describes as “more trouble”.

6. Recommendation

ConcurrencyThrottleInterceptor

is intended for a single, system‑wide resource guard rather than fine‑grained business rate limiting. For per‑API or per‑method limits, use dedicated libraries such as Bucket4j or Resilience4j.

Bucket4j – API rate limiting made simple

Resilience4j – Ensuring system stability and interface safety

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AOPspring-bootRate LimitingBeanPostProcessorResilience4jBucket4jConcurrencyThrottleInterceptor
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.