Backend Development 21 min read

Understanding Interface Idempotency and Distributed Rate Limiting: Concepts, Algorithms, and Java Implementations

This article explains the principle of interface idempotency, presents practical techniques such as version‑based updates and token mechanisms, and then delves into distributed rate‑limiting dimensions, common algorithms like token‑bucket and leaky‑bucket, and concrete implementations using Guava, Nginx, Redis and Lua with full code examples.

Architecture Digest
Architecture Digest
Architecture Digest
Understanding Interface Idempotency and Distributed Rate Limiting: Concepts, Algorithms, and Java Implementations

1. Interface Idempotency

Interface idempotency means that multiple identical requests produce the same result without side effects; a classic example is a payment request that, if retried due to network failure, should not cause double charging.

The core idea is to use a unique business identifier (or version number) to guarantee idempotency: before performing an operation, check whether the identifier has already been processed; if not, execute the operation, otherwise skip it. In concurrent scenarios, the check‑and‑execute step must be protected by a lock.

1. Idempotency of Update Operations

1) Update by unique business ID

Use a version field to control update idempotency. The client reads the current data, the server returns the version number in a hidden field, and the client submits the version together with the update. The backend updates only when the version matches.

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

2. Token Mechanism for Update/Insert Idempotency

When there is no natural business ID, generate a token on the server (e.g., during page rendering) and store it in a hidden field. The client submits the token with the request; the backend uses the token to acquire a distributed lock, performs the insert/update, and does not release the lock until it expires, thus preventing duplicate execution.

2. Distributed Rate Limiting

Rate limiting can be defined along several dimensions:

Time window (e.g., per second, per minute)

Resource limits (maximum number of accesses or connections)

In practice, multiple rules are often combined, such as limiting QPS per IP, total connections per server, transmission speed per user group, and applying black‑/white‑lists.

1. Dimensions of Distributed Rate Limiting

1) QPS and connection control – Set limits per IP or per server, e.g., each IP may send at most 10 requests per second and hold no more than 5 concurrent connections.

2) Transmission rate – Limit download speed based on user tier, e.g., regular users 100 KB/s, premium users 10 MB/s.

3) Black/white list – Dynamically block abusive IPs (blacklist) or grant privileged accounts unrestricted access (whitelist).

4) Distributed environment – Treat the whole cluster as a single logical node; limits apply globally regardless of which server receives the request.

2. Common Algorithms

1) Token Bucket Algorithm

The token bucket algorithm uses two key components:

Token : A request can proceed only if it obtains a token; otherwise it is queued or dropped.

Bucket : Holds a limited number of tokens; tokens are added at a fixed rate.

Token generation – A generator adds tokens to the bucket at a configured rate (e.g., 100 tokens per second). If the bucket is full, excess tokens are discarded.

Token acquisition – Each incoming request must take a token. If tokens are exhausted, the request can be queued in a buffer or rejected.

2) Leaky Bucket Algorithm

Leaky bucket stores incoming requests in a queue (the bucket) and drains them at a constant rate, regardless of the arrival burst.

If the bucket is full, new requests are dropped. The algorithm guarantees a steady output rate, which prevents sudden traffic spikes.

Difference – Token bucket allows bursts up to the bucket capacity, while leaky bucket smooths traffic to a fixed rate.

3. Main Distributed Rate‑Limiting Solutions

1) Guava RateLimiter (client‑side)

Add the Guava dependency:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>18.0</version>
</dependency>

Example controller:

@RestController
@Slf4j
public class Controller {
    // 2 tokens per second
    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";
        }
    }

    @GetMapping("/tryAcquireWithTimeout")
    public String tryAcquireWithTimeout(Integer count, Integer timeout) {
        if (limiter.tryAcquire(count, timeout, TimeUnit.SECONDS)) {
            log.info("success, rate={}", limiter.getRate());
            return "success";
        } else {
            log.info("fail, rate={}", limiter.getRate());
            return "fail";
        }
    }

    @GetMapping("/acquire")
    public String acquire(Integer count) {
        limiter.acquire(count);
        log.info("success, rate={}", limiter.getRate());
        return "success";
    }
}

2) Nginx Rate Limiting

IP‑based limit example (nginx.conf):

# Define a shared memory zone for IP limits
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;
    }
}

Multi‑dimensional limit example adds zones for server name, per‑IP connections, etc., and configures limit_req and limit_conn directives accordingly.

3) Redis + Lua Distributed Limiting

Lua script (rateLimiter.lua) stored in Redis executes atomically:

-- Get the method key
local methodKey = KEYS[1]
redis.log(redis.LOG_DEBUG,'key is',methodKey)

-- Limit value passed from caller
local limit = tonumber(ARGV[1])

-- Current count (default 0)
local count = tonumber(redis.call('get',methodKey) or "0")

if count + 1 > limit then
    return false   -- reject
else
    redis.call('INCRBY',methodKey,1)
    redis.call('EXPIRE',methodKey,1)
    return true    -- allow
end

Spring Boot integration:

@Service
@Slf4j
public class AccessLimiter {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisScript
rateLimitLua;

    public void limitAccess(String key, Integer limit) {
        boolean acquired = stringRedisTemplate.execute(rateLimitLua, Lists.newArrayList(key), limit.toString());
        if (!acquired) {
            log.error("Your access is blocked, key={}", key);
            throw new RuntimeException("Your access is blocked");
        }
    }
}

Configuration class loads the Lua script:

@Configuration
public class RedisConfiguration {
    @Bean
    public RedisTemplate
redisTemplate(RedisConnectionFactory factory) {
        return new StringRedisTemplate(factory);
    }

    @Bean
    public DefaultRedisScript
loadRedisScript() {
        DefaultRedisScript
redisScript = new DefaultRedisScript<>();
        redisScript.setLocation(new ClassPathResource("rateLimiter.lua"));
        redisScript.setResultType(Boolean.class);
        return redisScript;
    }
}

Custom annotation and AOP aspect enforce the limit on annotated methods:

@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.gyx.demo.annotation.AccessLimiterAop)")
    public void cut() {}

    @Before("cut()")
    public void before(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        AccessLimiterAop annotation = method.getAnnotation(AccessLimiterAop.class);
        if (annotation == null) return;
        String key = annotation.methodKey();
        int limit = annotation.limit();
        if (StringUtils.isEmpty(key)) {
            key = method.getName();
            Class[] types = method.getParameterTypes();
            if (types != null) {
                String paramTypes = Arrays.stream(types)
                    .map(Class::getName)
                    .collect(Collectors.joining(","));
                key += "#" + paramTypes;
            }
        }
        accessLimiter.limitAccess(key, limit);
    }
}

Apply the annotation on a controller method:

@RestController
@Slf4j
public class TestController {
    @Autowired
    private AccessLimiter accessLimiter;

    @GetMapping("/test")
    @AccessLimiterAop(limit = 1)
    public String test() {
        return "success";
    }
}

Overall, the article demonstrates how to achieve idempotent updates and robust distributed rate limiting using a combination of business identifiers, token mechanisms, algorithmic models, and concrete Java/NGINX/Redis implementations.

JavaBackend DevelopmentRedisIdempotencyNginxtoken bucketleaky bucketDistributed Rate Limiting
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

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.