How to Ensure API Idempotency and Implement Distributed Rate Limiting in Java
This guide explains the principles of API idempotency using unique business IDs or token mechanisms, explores distributed rate‑limiting dimensions, compares token‑bucket and leaky‑bucket algorithms, and provides concrete implementations with Guava RateLimiter, Nginx configuration, and a Redis‑Lua script integrated into Spring Boot, including annotation‑based AOP for easy usage.
API Idempotency
Idempotency ensures that multiple identical requests produce the same result without side effects (e.g., duplicate payments). The typical approach is to use a unique business identifier or a token together with version checking or distributed locks.
Update operation idempotency
Store a version number in a hidden field and include it in the UPDATE statement as a condition:
UPDATE table_name
SET version = version + 1, column = #{value}
WHERE id = #{id} AND version = #{version};Token‑based idempotency for update/insert
Generate a token on the registration page, return it in a hidden field, and use the token to acquire a distributed lock before performing the INSERT/UPDATE. The lock is held until it expires, preventing duplicate execution.
Distributed Rate Limiting
Rate limiting can be applied along several dimensions:
Time window (QPS, requests per minute, etc.)
Resource limits (max connections, bandwidth)
Black/white lists
Distributed environment (rules shared across a cluster)
Multiple rules can be combined, e.g., limit each IP to 10 RPS and each server to 1000 QPS.
Common algorithms
Token Bucket
A bucket with a fixed capacity receives tokens at a constant rate. A request proceeds only if a token is available; excess tokens are discarded when the bucket is full.
Leaky Bucket
Incoming requests are queued in a bucket and released at a constant rate, smoothing bursts and preventing sudden spikes.
Implementation options
Guava RateLimiter (client‑side)
Add Guava 18.0 to Maven and create a RateLimiter. Example controller:
@RestController
@Slf4j
public class RateController {
private final RateLimiter limiter = RateLimiter.create(2.0); // 2 tokens per second
@GetMapping("/tryAcquire")
public String tryAcquire(Integer count) {
if (limiter.tryAcquire(count)) {
log.info("Success, rate {}", limiter.getRate());
return "success";
}
log.info("Rejected, rate {}", limiter.getRate());
return "fail";
}
@GetMapping("/tryAcquireWithTimeout")
public String tryAcquireWithTimeout(Integer count, Integer timeout) {
if (limiter.tryAcquire(count, timeout, TimeUnit.SECONDS)) {
log.info("Success, rate {}", limiter.getRate());
return "success";
}
log.info("Rejected, rate {}", limiter.getRate());
return "fail";
}
@GetMapping("/acquire")
public String acquire(Integer count) {
limiter.acquire(count);
log.info("Success, rate {}", limiter.getRate());
return "success";
}
}Nginx rate limiting
Define a shared memory zone and apply it in a location block. Example limits each IP to 1 request/s with a burst of 2:
limit_req_zone $binary_remote_addr zone=iplimit:20m rate=1r/s;
server {
server_name www.test.com;
location /access-limit/ {
proxy_pass http://127.0.0.1:8080/;
limit_req zone=iplimit burst=2 nodelay;
}
}Redis + Lua distributed limiting
Lua scripts run atomically inside Redis. The script increments a counter, sets a 1‑second TTL, and returns true if the request is allowed.
-- rateLimiter.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local count = tonumber(redis.call('GET', key) or '0')
if count + 1 > limit then
return false
else
redis.call('INCRBY', key, 1)
redis.call('EXPIRE', key, 1)
return true
endSpring integration:
@Service
@Slf4j
public class AccessLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedisScript<Boolean> rateLimitLua;
public void limitAccess(String key, Integer limit) {
Boolean allowed = redisTemplate.execute(rateLimitLua,
Collections.singletonList(key),
limit.toString());
if (!allowed) {
log.error("Access blocked, key={}", key);
throw new RuntimeException("Access blocked");
}
}
}Annotation‑based AOP
Define a custom annotation and an aspect that builds a unique key (method name + parameter types) and delegates to AccessLimiter.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiterAop {
int limit();
String methodKey() default "";
}
@Aspect
@Component
@Slf4j
public class AccessLimiterAspect {
@Autowired
private AccessLimiter accessLimiter;
@Pointcut("@annotation(com.example.AccessLimiterAop)")
public void cut() {}
@Before("cut()")
public void before(JoinPoint joinPoint) {
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
Method method = sig.getMethod();
AccessLimiterAop ann = method.getAnnotation(AccessLimiterAop.class);
if (ann == null) return;
String key = ann.methodKey();
if (StringUtils.isEmpty(key)) {
key = method.getName() + "#" +
Arrays.stream(method.getParameterTypes())
.map(Class::getName)
.collect(Collectors.joining(","));
}
accessLimiter.limitAccess(key, ann.limit());
}
}Usage on a controller method:
@RestController
@Slf4j
public class TestController {
@AccessLimiterAop(limit = 1)
@GetMapping("/test")
public String test() {
return "success";
}
}Choosing an implementation
Guava RateLimiter : lightweight, client‑side, suitable for single‑instance services.
Nginx limit_req : enforces limits at the edge, works for any backend.
Redis + Lua : provides a centralized, atomic limiter for distributed clusters; can be combined with Spring AOP for declarative usage.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IT Architects Alliance
Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
