A comprehensive Java Spring Boot service implementing multiple rate limiting strategies with annotation-based and programmatic APIs.
-
Multiple Rate Limiting Algorithms
- Token Bucket
- Fixed Window Counter
- Sliding Window Log
- Sliding Window Counter
- Leaky Bucket
-
Flexible Usage
- Annotation-based rate limiting with AOP
- Programmatic API
- SpEL expression support for dynamic keys
- Per-user, per-IP, or custom key rate limiting
-
Production Ready
- Thread-safe implementations
- Exception handling with proper HTTP status codes
- Configurable limits and windows
- Retry-After headers
- Java 17 or higher
- Maven 3.6+
# Build the project
mvn clean package
# Run the application
mvn spring-boot:run
# Or run the JAR
java -jar target/rate-limiter-service-1.0.0.jarThe service will start on http://localhost:8080
How it works: Tokens are added to a bucket at a constant rate. Each request consumes one token. If no tokens are available, the request is rejected.
Pros:
- Handles bursts well
- Smooth rate limiting
- Allows temporary spikes
Cons:
- More complex than fixed window
- Requires token refill tracking
Use case: APIs that need to handle occasional bursts while maintaining average rate
@GetMapping("/api/resource")
@RateLimit(
type = RateLimiterType.TOKEN_BUCKET,
capacity = 10,
refillRate = 2.0 // 2 tokens per second
)
public ResponseEntity<?> getResource() {
return ResponseEntity.ok("Success");
}How it works: Time is divided into fixed windows. Counts requests per window and resets at window boundaries.
Pros:
- Simple to implement
- Memory efficient
- Easy to understand
Cons:
- Can allow bursts at window boundaries (up to 2x limit)
- Not perfectly accurate
Use case: Simple rate limiting where burst allowance is acceptable
@GetMapping("/api/resource")
@RateLimit(
type = RateLimiterType.FIXED_WINDOW_COUNTER,
limit = 100,
windowSeconds = 60
)
public ResponseEntity<?> getResource() {
return ResponseEntity.ok("Success");
}How it works: Maintains a log of all request timestamps. Removes old timestamps outside the current window and counts remaining ones.
Pros:
- Most accurate
- No burst issues
- Precise rate limiting
Cons:
- Higher memory usage (stores all timestamps)
- More processing overhead
Use case: Critical APIs requiring precise rate limiting
@GetMapping("/api/resource")
@RateLimit(
type = RateLimiterType.SLIDING_WINDOW_LOG,
limit = 100,
windowSeconds = 60
)
public ResponseEntity<?> getResource() {
return ResponseEntity.ok("Success");
}How it works: Hybrid approach combining fixed window counters with weighted counting based on time position in window.
Pros:
- More accurate than fixed window
- Less memory than sliding log
- Good balance
Cons:
- Slightly more complex calculations
- Approximation (not 100% accurate)
Use case: General purpose rate limiting with good accuracy
@GetMapping("/api/resource")
@RateLimit(
type = RateLimiterType.SLIDING_WINDOW_COUNTER,
limit = 100,
windowSeconds = 60
)
public ResponseEntity<?> getResource() {
return ResponseEntity.ok("Success");
}How it works: Requests are added to a queue (bucket). Requests "leak" from the bucket at a constant rate.
Pros:
- Smooth output rate
- Prevents traffic spikes
- Constant processing rate
Cons:
- Can reject requests even when system has capacity
- Queue management overhead
Use case: Protecting downstream services with strict rate requirements
@GetMapping("/api/resource")
@RateLimit(
type = RateLimiterType.LEAKY_BUCKET,
capacity = 10,
refillRate = 1.0 // Process 1 request per second
)
public ResponseEntity<?> getResource() {
return ResponseEntity.ok("Success");
}@GetMapping("/api/public")
@RateLimit(
type = RateLimiterType.TOKEN_BUCKET,
limit = 10,
windowSeconds = 60
)
public ResponseEntity<?> publicEndpoint() {
return ResponseEntity.ok("Success");
}@GetMapping("/api/user/{userId}/data")
@RateLimit(
type = RateLimiterType.SLIDING_WINDOW_COUNTER,
limit = 50,
windowSeconds = 3600,
key = "#userId" // SpEL expression
)
public ResponseEntity<?> getUserData(@PathVariable String userId) {
return ResponseEntity.ok("User data");
}@GetMapping("/api/premium")
@RateLimit(
type = RateLimiterType.TOKEN_BUCKET,
limit = 1000,
windowSeconds = 3600,
key = "#request.getHeader('X-API-Key')"
)
public ResponseEntity<?> premiumEndpoint() {
return ResponseEntity.ok("Premium feature");
}@PostMapping("/api/expensive-operation")
@RateLimit(
type = RateLimiterType.LEAKY_BUCKET,
capacity = 5,
refillRate = 0.1, // Very slow: 1 request per 10 seconds
key = "'global'"
)
public ResponseEntity<?> expensiveOperation() {
return ResponseEntity.ok("Operation completed");
}@RestController
@RequiredArgsConstructor
public class CustomController {
private final RateLimiterService rateLimiterService;
@GetMapping("/api/custom")
public ResponseEntity<?> customRateLimit(@RequestParam String userId) {
// Build configuration
RateLimitConfig config = RateLimitConfig.builder()
.type(RateLimiterType.TOKEN_BUCKET)
.limit(10)
.windowSizeInSeconds(60)
.refillRate(1.0)
.capacity(10)
.build();
// Check rate limit
RateLimitResult result = rateLimiterService.tryAcquire(userId, config);
if (!result.isAllowed()) {
return ResponseEntity
.status(HttpStatus.TOO_MANY_REQUESTS)
.header("Retry-After", String.valueOf(result.getRetryAfterSeconds()))
.body("Rate limit exceeded");
}
// Process request
return ResponseEntity.ok("Success");
}
}| Endpoint | Strategy | Limit | Window |
|---|---|---|---|
GET /api/token-bucket |
Token Bucket | 5 | 1s refill |
GET /api/fixed-window |
Fixed Window Counter | 5 | 60s |
GET /api/sliding-log |
Sliding Window Log | 5 | 60s |
GET /api/sliding-counter |
Sliding Window Counter | 5 | 60s |
GET /api/leaky-bucket |
Leaky Bucket | 5 | 1s leak |
GET /api/user/{userId} |
Token Bucket (per user) | 10 | 60s |
POST /api/check-limit?key=user123
Content-Type: application/json
{
"type": "TOKEN_BUCKET",
"limit": 10,
"windowSizeInSeconds": 60,
"refillRate": 1.0,
"capacity": 10
}DELETE /api/reset/{type}/{key}
# Example
DELETE /api/reset/TOKEN_BUCKET/user123GET /api/health# Test token bucket endpoint
for i in {1..10}; do
curl http://localhost:8080/api/token-bucket
echo ""
done
# Test with user ID
curl http://localhost:8080/api/user/alice
# Reset rate limiter
curl -X DELETE http://localhost:8080/api/reset/TOKEN_BUCKET/127.0.0.1# Test fixed window
http GET http://localhost:8080/api/fixed-window
# Test programmatic API
http POST http://localhost:8080/api/check-limit?key=user123 \
type=TOKEN_BUCKET \
limit=10 \
windowSizeInSeconds=60 \
refillRate=1.0 \
capacity=10{
"success": true,
"message": "Request processed successfully",
"timestamp": "2024-01-15T10:30:00"
}{
"timestamp": "2024-01-15T10:30:00",
"status": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded - Token bucket depleted",
"retryAfterSeconds": 5
}Headers:
HTTP/1.1 429 Too Many Requests
Retry-After: 5
# Server Configuration
server.port=8080
# Logging
logging.level.com.example.ratelimiter=DEBUG
# Redis (for distributed rate limiting - optional)
spring.data.redis.host=localhost
spring.data.redis.port=6379You can customize rate limiting behavior by modifying the annotation parameters:
@RateLimit(
type = RateLimiterType.TOKEN_BUCKET, // Algorithm
limit = 100, // Max requests
windowSeconds = 3600, // Time window
refillRate = 2.0, // Refill rate (tokens/sec)
capacity = 100, // Bucket capacity
key = "#userId" // Rate limit key
)| Algorithm | Accuracy | Memory | Complexity | Bursts | Use Case |
|---|---|---|---|---|---|
| Token Bucket | High | Low | Medium | ✅ Yes | General purpose, bursty traffic |
| Fixed Window | Medium | Low | Low | Simple scenarios | |
| Sliding Window Log | Very High | High | Medium | ❌ No | Critical APIs |
| Sliding Window Counter | High | Medium | Medium | Balanced approach | |
| Leaky Bucket | High | Low | Medium | ❌ No | Smooth output required |
┌─────────────────────────────────────────┐
│ Controller Layer │
│ (Endpoints with @RateLimit annotation) │
└────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ RateLimitAspect (AOP) │
│ - Intercepts annotated methods │
│ - Evaluates SpEL expressions │
│ - Throws RateLimitExceededException │
└────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ RateLimiterService │
│ - Strategy selection │
│ - Delegates to specific strategy │
└────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Strategy Implementations │
│ - TokenBucketStrategy │
│ - FixedWindowCounterStrategy │
│ - SlidingWindowLogStrategy │
│ - SlidingWindowCounterStrategy │
│ - LeakyBucketStrategy │
└─────────────────────────────────────────┘
- Implement
RateLimiterStrategyinterface - Add the strategy to
RateLimiterTypeenum - Register in
RateLimiterService.init()
Example:
@Component
public class CustomStrategy implements RateLimiterStrategy {
@Override
public RateLimitResult tryAcquire(String key, RateLimitConfig config) {
// Your implementation
}
@Override
public void reset(String key) {
// Reset logic
}
}For distributed systems, you can extend the strategies to use Redis:
@Component
public class RedisTokenBucketStrategy implements RateLimiterStrategy {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public RateLimitResult tryAcquire(String key, RateLimitConfig config) {
// Use Redis for distributed state
String redisKey = "rate_limit:" + key;
// Implementation using Redis Lua scripts
}
}-
Choose the Right Algorithm
- Token Bucket: Best for most APIs
- Sliding Window Log: When accuracy is critical
- Fixed Window: When simplicity is key
- Leaky Bucket: When smooth output is required
-
Set Appropriate Limits
- Consider your system capacity
- Account for peak traffic
- Leave room for bursts
-
Use Meaningful Keys
- User ID for per-user limits
- API key for different tiers
- IP address for anonymous access
- Endpoint name for per-endpoint limits
-
Monitor and Adjust
- Track rate limit hits
- Adjust limits based on usage patterns
- Log exceeded requests
-
Provide Clear Feedback
- Return appropriate HTTP status codes
- Include Retry-After headers
- Provide helpful error messages
MIT License
Built with Spring Boot and Java