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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
How to Build a Lightweight, Annotation‑Driven Rate Limiter with Spring AOP and Guava

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.

backendJavaAnnotationRate Limitingspring-aopGuava RateLimiter
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.