Backend Development 19 min read

Idempotent Interfaces and Distributed Rate Limiting: Concepts, Algorithms, and Practical Implementations

This article explains the importance of interface idempotency and presents a comprehensive guide to distributed rate limiting, covering key dimensions, token‑bucket and leaky‑bucket algorithms, and concrete implementations using Guava RateLimiter, Nginx, and Redis‑Lua with Java code examples.

Top Architect
Top Architect
Top Architect
Idempotent Interfaces and Distributed Rate Limiting: Concepts, Algorithms, and Practical Implementations

Interface idempotency ensures that repeated requests produce the same result, preventing side‑effects such as double charges in payment scenarios.

The core idea is to use a unique business identifier or a token combined with locking to guarantee that an update or insert operation is executed only once.

Example SQL for optimistic locking:

update set version = version +1 , xxx=${xxx} where id =${id} and version = ${version};

A token mechanism can generate a token on the registration page, store it in a hidden field, and use it to acquire a distributed lock before performing the insert.

Distributed Rate Limiting

Rate limiting can be defined along several dimensions: time windows (e.g., per second or per minute), resource limits (max requests or connections), black/white lists, and the distributed environment where limits apply across all nodes.

Common algorithms:

Token Bucket

A bucket holds a configurable number of tokens that are added at a steady rate; a request proceeds only if it can take a token.

RateLimiter limiter = RateLimiter.create(2.0); // 2 tokens per second

Non‑blocking acquisition:

if (limiter.tryAcquire(count)) { log.info("success, rate={}", limiter.getRate()); } else { log.info("fail, rate={}", limiter.getRate()); }

Leaky Bucket

Requests are placed in a queue (the bucket) and are released at a constant rate; excess requests are discarded when the bucket is full.

while (true) { if (bucket.isFull()) { reject(); } else { bucket.add(request); }

Implementation Solutions

Guava RateLimiter (client‑side)

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>18.0</version>
</dependency>
@RestController
@Slf4j
public class Controller {
    RateLimiter limiter = RateLimiter.create(2.0);
    @GetMapping("/tryAcquire")
    public String tryAcquire(Integer count) {
        if (limiter.tryAcquire(count)) {
            log.info("success, rate={}", limiter.getRate());
            return "success";
        } else {
            log.info("fail, rate={}", limiter.getRate());
            return "fail";
        }
    }
}

Nginx Rate Limiting

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

A Lua script runs atomically inside Redis to check the current count and increment it if the limit is not exceeded.

-- limit.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
end

Spring integration:

@Service
@Slf4j
public class AccessLimiter {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisScript
rateLimitLua;
    public void limitAccess(String key, Integer limit) {
        boolean allowed = stringRedisTemplate.execute(rateLimitLua, Collections.singletonList(key), limit.toString());
        if (!allowed) {
            log.error("Access blocked for key={}", key);
            throw new RuntimeException("Access blocked");
        }
    }
}

An annotation‑based AOP aspect can apply the limiter to any controller method.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimiterAop {
    int limit();
    String methodKey() default "";
}
@Aspect
@Component
public class AccessLimiterAspect {
    @Autowired
    private AccessLimiter accessLimiter;
    @Pointcut("@annotation(com.example.AccessLimiterAop)")
    public void cut() {}
    @Before("cut()")
    public void before(JoinPoint jp) {
        MethodSignature ms = (MethodSignature) jp.getSignature();
        Method method = ms.getMethod();
        AccessLimiterAop anno = method.getAnnotation(AccessLimiterAop.class);
        String key = StringUtils.isEmpty(anno.methodKey()) ? method.getName() : anno.methodKey();
        accessLimiter.limitAccess(key, anno.limit());
    }
}
JavaRedisIdempotencynginxRate Limitingtoken bucketleaky bucket
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.