package com.example.demo.interceptor;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) throws Exception {
String key = getClientKey(request);
Bucket bucket = resolveBucket(key);
if (bucket.tryConsume(1)) {
long remaining = bucket.getAvailableTokens();
response.addHeader("X-Rate-Limit-Remaining", String.valueOf(remaining));
return true;
} else {
long waitForRefill = bucket.estimateAbilityToConsume(1).getNanosToWaitForRefill() / 1_000_000_000;
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.addHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
response.getWriter().write("Rate limit exceeded. Try again in " + waitForRefill + " seconds");
return false;
}
}
private Bucket resolveBucket(String key) {
return cache.computeIfAbsent(key, k -> createNewBucket());
}
private Bucket createNewBucket() {
// 100 requests per minute
Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)));
return Bucket.builder()
.addLimit(limit)
.build();
}
private String getClientKey(HttpServletRequest request) {
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null) {
return apiKey;
}
return request.getRemoteAddr();
}
}
package com.example.demo.config;
import com.example.demo.interceptor.RateLimitInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class RateLimitConfig implements WebMvcConfigurer {
private final RateLimitInterceptor rateLimitInterceptor;
public RateLimitConfig(RateLimitInterceptor rateLimitInterceptor) {
this.rateLimitInterceptor = rateLimitInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/login", "/api/health");
}
}
package com.example.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int requests() default 100;
int perSeconds() default 60;
}
// Usage:
// @RateLimit(requests = 10, perSeconds = 60)
// @GetMapping("/expensive-operation")
// public ResponseEntity<?> expensiveOperation() {
// return ResponseEntity.ok("Success");
// }
Rate limiting prevents API abuse and ensures fair resource usage. I implement rate limiting using Bucket4j for token bucket algorithm or Redis for distributed scenarios. Limits apply per user, IP, or API key. HTTP 429 (Too Many Requests) indicates limit exceeded. Headers communicate remaining requests and reset time. Sliding window algorithms provide smooth rate limiting. Tiered limits support different user levels. Rate limiting protects against DoS attacks and resource exhaustion. Distributed systems use Redis to share rate limit state. Custom annotations simplify application. Graceful degradation prioritizes critical operations. Monitoring tracks rate limit hits. Proper rate limiting balances system protection with user experience.