intermediatejava
Rate Limiter Annotation
A reusable AOP annotation to rate-limit API endpoints using a Token Bucket algorithm in Spring Boot.
Published December 2, 2025
Updated December 2, 2025
System Designspring-bootaopperformanceconcurrency
Rate Limiter Annotation
A custom Spring Boot annotation to protect your API endpoints from abuse. This implementation uses Spring AOP to intercept method calls and enforces a rate limit using a thread-safe Token Bucket algorithm.
Highlights
- Zero External Dependencies: Uses standard Java concurrency primitives (
ConcurrentHashMap,ReentrantLock). - Configurable: Set requests per second/minute directly on the annotation.
- IP-based Limiting: Automatically identifies clients based on their IP address (requires
HttpServletRequest).
Code
package com.example.demo.aspect;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
// 1. The Annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int limit() default 10;
long duration() default 60;
TimeUnit unit() default TimeUnit.SECONDS;
}
// 2. The Aspect
@Aspect
@Component
public class RateLimitAspect {
private final Map<String, TokenBucket> buckets = new ConcurrentHashMap<>();
@Around("@annotation(rateLimit)")
public Object enforceRateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String ip = request.getRemoteAddr();
String key = ip + ":" + joinPoint.getSignature().toShortString();
TokenBucket bucket = buckets.computeIfAbsent(key, k -> new TokenBucket(rateLimit.limit(), rateLimit.duration(), rateLimit.unit()));
if (!bucket.tryConsume()) {
throw new RateLimitExceededException("Too many requests. Please try again later.");
}
return joinPoint.proceed();
}
// Simple Token Bucket Implementation
private static class TokenBucket {
private final long capacity;
private final long refillPeriodNanos;
private double tokens;
private long lastRefillTime;
public TokenBucket(long capacity, long duration, TimeUnit unit) {
this.capacity = capacity;
this.refillPeriodNanos = unit.toNanos(duration) / capacity;
this.tokens = capacity;
this.lastRefillTime = System.nanoTime();
}
public synchronized boolean tryConsume() {
refill();
if (tokens >= 1) {
tokens--;
return true;
}
return false;
}
private void refill() {
long now = System.nanoTime();
long elapsed = now - lastRefillTime;
if (elapsed > 0) {
double newTokens = (double) elapsed / refillPeriodNanos;
tokens = Math.min(capacity, tokens + newTokens);
lastRefillTime = now;
}
}
}
// Custom Exception
public static class RateLimitExceededException extends RuntimeException {
public RateLimitExceededException(String message) {
super(message);
}
}
}Usage
Simply annotate your controller methods:
@RestController
@RequestMapping("/api")
public class ProductController {
@GetMapping("/products")
@RateLimit(limit = 5, duration = 1, unit = TimeUnit.MINUTES) // 5 requests per minute
public List<Product> getProducts() {
return productService.findAll();
}
@PostMapping("/orders")
@RateLimit(limit = 1, duration = 10, unit = TimeUnit.SECONDS) // 1 request every 10 seconds
public Order createOrder(@RequestBody Order order) {
return orderService.create(order);
}
}[!NOTE] For distributed systems (e.g., multiple instances of your service), replace the in-memory
ConcurrentHashMapwith a distributed store like Redis (usingRedissonorLettuce).