Basic rate limiting
The simplest way to add rate limiting is with the@RateLimit annotation on a service method.
Global rate limit
import io.github.v4runsharma.ratelimiter.annotation.RateLimit;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Service;
@Service
public class BillingService {
@RateLimit(
name = "invoice-create",
scope = "GLOBAL",
limit = 10,
duration = 1,
timeUnit = TimeUnit.MINUTES
)
public String createInvoice(String accountId) {
return "Invoice created for " + accountId;
}
}
createInvoice to 10 requests per minute across all users.
Per-user rate limiting
Using custom key resolver
For per-user or per-tenant limits, implement a customRateLimitKeyResolver:
import io.github.v4runsharma.ratelimiter.core.RateLimitContext;
import io.github.v4runsharma.ratelimiter.key.RateLimitKeyResolver;
import org.springframework.stereotype.Component;
@Component
public class UserIdKeyResolver implements RateLimitKeyResolver {
@Override
public String resolveKey(RateLimitContext context) {
Object[] args = context.getArguments();
String userId = String.valueOf(args[0]);
return "user:" + userId + ":" + context.getMethod().getName();
}
}
@Service
public class OrderService {
@RateLimit(
name = "order-create-per-user",
scope = "USER",
keyResolver = UserIdKeyResolver.class,
limit = 5,
duration = 1,
timeUnit = TimeUnit.MINUTES
)
public String createOrder(String userId, String productId) {
return "Order created for user " + userId;
}
}
Per-IP rate limiting
IP-based key resolver
import io.github.v4runsharma.ratelimiter.core.RateLimitContext;
import io.github.v4runsharma.ratelimiter.key.RateLimitKeyResolver;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Component
public class IpAddressKeyResolver implements RateLimitKeyResolver {
@Override
public String resolveKey(RateLimitContext context) {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String ipAddress = request.getRemoteAddr();
return "ip:" + ipAddress + ":" + context.getMethod().getName();
}
return "ip:unknown:" + context.getMethod().getName();
}
}
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class PublicApiController {
@RateLimit(
name = "public-search",
scope = "IP",
keyResolver = IpAddressKeyResolver.class,
limit = 100,
duration = 1,
timeUnit = TimeUnit.HOURS
)
@GetMapping("/search")
public String search(@RequestParam String query) {
return "Search results for: " + query;
}
}
Class-level rate limiting
Apply@RateLimit to an entire class to protect all methods:
import org.springframework.stereotype.Service;
@Service
@RateLimit(
name = "payment-service",
scope = "GLOBAL",
limit = 1000,
duration = 1,
timeUnit = TimeUnit.HOURS
)
public class PaymentService {
public void processPayment(String transactionId) {
// Protected by class-level rate limit
}
@RateLimit(
name = "refund-process",
scope = "GLOBAL",
limit = 100,
duration = 1,
timeUnit = TimeUnit.HOURS
)
public void processRefund(String transactionId) {
// Method-level annotation overrides class-level
}
}
Different time windows
Short window
@RateLimit(
name = "otp-send",
scope = "USER",
keyResolver = UserIdKeyResolver.class,
limit = 3,
duration = 30,
timeUnit = TimeUnit.SECONDS
)
public void sendOtp(String userId) {
// 3 OTPs per 30 seconds per user
}
Long window
@RateLimit(
name = "report-generate",
scope = "USER",
keyResolver = UserIdKeyResolver.class,
limit = 10,
duration = 24,
timeUnit = TimeUnit.HOURS
)
public void generateReport(String userId) {
// 10 reports per day per user
}
API key-based rate limiting
API key resolver
import io.github.v4runsharma.ratelimiter.core.RateLimitContext;
import io.github.v4runsharma.ratelimiter.key.RateLimitKeyResolver;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Component
public class ApiKeyResolver implements RateLimitKeyResolver {
@Override
public String resolveKey(RateLimitContext context) {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.isBlank()) {
return "apikey:" + apiKey + ":" + context.getMethod().getName();
}
}
return "apikey:unknown:" + context.getMethod().getName();
}
}
REST endpoint with API key limiting
@RestController
@RequestMapping("/api/v1")
public class ApiController {
@RateLimit(
name = "api-data-fetch",
scope = "USER",
keyResolver = ApiKeyResolver.class,
limit = 1000,
duration = 1,
timeUnit = TimeUnit.DAYS
)
@GetMapping("/data")
public String getData() {
return "Protected data";
}
}
Multiple limits on the same method
You can layer multiple rate limits by creating wrapper methods:@Service
public class UploadService {
@RateLimit(
name = "upload-per-minute",
scope = "USER",
keyResolver = UserIdKeyResolver.class,
limit = 10,
duration = 1,
timeUnit = TimeUnit.MINUTES
)
public String uploadFileRateLimitMinute(String userId, byte[] data) {
return uploadFileRateLimitHour(userId, data);
}
@RateLimit(
name = "upload-per-hour",
scope = "USER",
keyResolver = UserIdKeyResolver.class,
limit = 100,
duration = 1,
timeUnit = TimeUnit.HOURS
)
public String uploadFileRateLimitHour(String userId, byte[] data) {
return uploadFileInternal(userId, data);
}
private String uploadFileInternal(String userId, byte[] data) {
// Actual upload logic
return "File uploaded successfully";
}
}
Static key for grouping operations
Use thekey attribute to group different methods under the same limit:
@Service
public class NotificationService {
@RateLimit(
name = "notifications",
scope = "USER",
key = "all-notifications",
keyResolver = UserIdKeyResolver.class,
limit = 50,
duration = 1,
timeUnit = TimeUnit.HOURS
)
public void sendEmail(String userId, String message) {
// Send email
}
@RateLimit(
name = "notifications",
scope = "USER",
key = "all-notifications",
keyResolver = UserIdKeyResolver.class,
limit = 50,
duration = 1,
timeUnit = TimeUnit.HOURS
)
public void sendSms(String userId, String message) {
// Send SMS
}
}
Disabling rate limiting conditionally
Use theenabled flag to disable specific limits:
@Service
public class AdminService {
@RateLimit(
name = "admin-operation",
scope = "GLOBAL",
limit = 10,
duration = 1,
timeUnit = TimeUnit.MINUTES,
enabled = false // Temporarily disabled
)
public void performAdminTask() {
// Not rate limited when enabled=false
}
}
Configuration examples
Application properties
# Enable/disable rate limiting globally
ratelimiter.enabled=true
# Redis key prefix for all rate limit buckets
ratelimiter.redis-key-prefix=myapp:ratelimiter
# Fail-open: allow requests when Redis is unavailable
ratelimiter.fail-open=false
# Include rate limit headers in 429 responses
ratelimiter.include-http-headers=true
# Enable Micrometer metrics
ratelimiter.metrics-enabled=true
# Redis connection
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=
spring.data.redis.database=0
Application YAML
ratelimiter:
enabled: true
redis-key-prefix: myapp:ratelimiter
fail-open: false
include-http-headers: true
metrics-enabled: true
spring:
data:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
Testing rate limits
Integration test example
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import io.github.v4runsharma.ratelimiter.exception.RateLimitExceededException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Testcontainers
class RateLimitIntegrationTest {
@Container
private static final GenericContainer<?> redis =
new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@Autowired
private BillingService billingService;
@Test
void shouldEnforceRateLimit() {
// First 10 requests should succeed
for (int i = 0; i < 10; i++) {
assertThat(billingService.createInvoice("account123"))
.isNotNull();
}
// 11th request should be rate limited
assertThrows(
RateLimitExceededException.class,
() -> billingService.createInvoice("account123")
);
}
}