How to Build a Lightweight, Annotation‑Driven Rate Limiter with Spring AOP and Guava
This article explains how to create a non‑intrusive, configurable rate‑limiting component for Spring services by combining Spring AOP, a custom @RateLimit annotation, and Google Guava's RateLimiter, covering token‑bucket fundamentals, API usage, implementation details, common AOP pitfalls, and migration to distributed limiting.
Background
In a high‑traffic MQ‑driven batch job, a single‑node consumer can overwhelm the core database, causing CPU saturation and instability. Introducing a lightweight, non‑intrusive limiter avoids heavyweight Redis deployment while protecting the system.
Why Use AOP + Annotation
Decouples rate‑limit logic from business code.
Enables reuse across many methods without duplication.
Simplifies maintenance; changes affect only the aspect.
Guava RateLimiter Fundamentals
Guava implements a token‑bucket algorithm. Tokens are produced at a fixed rate; a request must acquire a token before proceeding. The library offers two modes:
SmoothBursty : default, allows short bursts.
SmoothWarmingUp : gradual ramp‑up, useful for resources that need warm‑up.
RateLimiter works only within a single JVM; each instance holds its token bucket in memory.
Key API
RateLimiter.create(double permitsPerSecond);
RateLimiter.create(double permitsPerSecond, long warmupPeriod, TimeUnit unit);
double acquire();
double acquire(int permits);
boolean tryAcquire();
boolean tryAcquire(long timeout, TimeUnit unit);Implementation Steps
Add Maven dependencies for com.google.guava:guava and org.springframework.boot:spring-boot-starter-aop.
Define a custom annotation @RateLimit with attributes: qps, block, timeout, timeUnit, warmupPeriod, warmupUnit, and message.
Create RateLimitException extending RuntimeException to signal rejection.
Implement RateLimitAop aspect:
Cache RateLimiter instances per method key using a ConcurrentHashMap.
Generate a unique method key from class name, method name, and parameter types.
Instantiate either SmoothBursty or SmoothWarmingUp based on warmupPeriod.
Handle blocking ( acquire or timed tryAcquire) and non‑blocking ( tryAcquire) modes according to annotation settings.
Log a warning and throw RateLimitException when acquisition fails.
Proceed with the original method when the token is obtained.
Apply @RateLimit on service methods. Example scenarios:
Critical sync method with block=true and a 500 ms timeout.
Non‑critical method with block=false that rejects excess traffic immediately.
Common AOP Pitfall: Self‑Invocation
When a method in the same bean calls another annotated method directly (e.g., this.pay()), the call bypasses the Spring proxy, so the aspect is not applied.
Reason: the internal call uses the target object, not the proxy.
Solutions:
Extract the called method into a separate bean and inject it.
Enable @EnableAspectJAutoProxy(exposeProxy = true) and invoke ((TradeService) AopContext.currentProxy()).pay().
Avoid autowiring the bean into itself, which can cause circular‑dependency issues.
From Single‑Node to Distributed Limiting
Because Guava RateLimiter is JVM‑local, scaling to many nodes requires a distributed solution. Replace the in‑process limiter with a Redis‑based limiter (e.g., Redisson RRateLimiter) inside the aspect while keeping the @RateLimit annotation unchanged.
// Pseudo‑code for seamless switch
private RRateLimiter getRedisLimiter(String key) {
RRateLimiter limiter = redissonClient.getRateLimiter(key);
// initialize limiter if needed
return limiter;
}
// In around advice
if (!limiter.tryAcquire(annotation.qps(), annotation.timeout(), annotation.timeUnit())) {
throw new RateLimitException("Distributed rate limit triggered");
}Conclusion
Combining Spring AOP with a custom annotation provides a clean, reusable rate‑limiting mechanism that starts as a lightweight single‑machine solution and can evolve into a distributed limiter without modifying business code, illustrating the power of aspect‑oriented design.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
