# How to Implement Idempotency Checks for API Requests in ContiNew-Admin

> Learn to implement idempotency checks for API requests in ContiNew-Admin using Redis distributed locks or the RateLimiter annotation for robust request deduplication.

- Repository: [OpenContiNew/continew-admin](https://github.com/continew-org/continew-admin)
- Tags: how-to-guide
- Published: 2026-02-28

---

**ContiNew-Admin provides two built-in patterns for API idempotency: Redis distributed locks via `RedisLockUtils` for exactly-once execution semantics, and the `@RateLimiter` annotation for frequency-based request deduplication.**

ContiNew-Admin is a modern open-source admin system built on Spring Boot that ships with robust infrastructure for handling concurrent API requests. Implementing idempotency checks ensures that mutating operations like order creation or file uploads execute only once, even when clients retry due to network timeouts. The codebase provides reusable utilities in `continew-starter-cache-redisson` that eliminate the need for custom locking logic.

## Redis Distributed Locks for Exactly-Once Semantics

The **Redis distributed lock** pattern guarantees that a critical section of code runs only once across multiple JVM instances or threads. This is essential for operations that must not produce duplicate side effects, such as creating database records or writing files.

### How the Lock Pattern Works in FileServiceImpl

The implementation in [`continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java`](https://github.com/continew-org/continew-admin/blob/main/continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java) demonstrates idempotent directory creation using `RedisLockUtils`. The `createParentDir` method (lines 317-329) constructs a unique lock key from the storage code and target path, then acquires the lock before performing filesystem operations.

```java
// Source: FileServiceImpl.java#L317-L329
String lockKey = StrUtil.format("Lock:{}:{}", storage.getCode(), parentPath);
try (RedisLockUtils lock = RedisLockUtils.tryLock(lockKey)) {
    if (!lock.isLocked()) {
        return; // duplicate request – lock already held
    }
    // ... perform the idempotent operation ...
}

```

The `tryLock` method returns immediately if the lock is unavailable, preventing duplicate folder creation when multiple threads attempt to initialize the same upload directory simultaneously.

### Implementing the Pattern in Your Controllers

To adopt this pattern for custom API endpoints:

1. **Construct a deterministic lock key** combining the user identifier with the operation name.
   ```java
   String lockKey = StrUtil.format("Lock:{}:{}", userId, "createOrder");
   ```

2. **Wrap critical code** in a try-with-resources block using `RedisLockUtils.tryLock`.
   ```java
   try (RedisLockUtils lock = RedisLockUtils.tryLock(lockKey)) {
       if (!lock.isLocked()) {
           return ResultVO.fail("Operation already in progress");
       }
       // Execute business logic
   }
   ```

3. **Configure TTL** when operations may exceed the default 30-second timeout.
   ```java
   try (RedisLockUtils lock = RedisLockUtils.tryLock(lockKey, Duration.ofMinutes(5))) {
       // Long-running operation
   }
   ```

## Rate Limiting with @RateLimiter for Frequency Control

The **`@RateLimiter`** annotation provides declarative throttling based on cache keys stored in Redis. Unlike distributed locks, this pattern limits how frequently a specific key can trigger an operation, making it ideal for preventing abuse or accidental duplicate submissions within a time window.

### Annotation-Based Throttling in CaptchaController

The `CaptchaController` in [`continew-server/src/main/java/top/continew/admin/controller/CaptchaController.java`](https://github.com/continew-org/continew-admin/blob/main/continew-server/src/main/java/top/continew/admin/controller/CaptchaController.java) (lines 150-156) uses `@RateLimiters` (container annotation) to prevent excessive captcha requests:

```java
// Source: CaptchaController.java#L150-L156
@RateLimiters({
    @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "MIN",
                 key = "#email + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.mail.templatePath')",
                 rate = 2, interval = 1, unit = TimeUnit.MINUTES,
                 message = "获取验证码操作太频繁，请稍后再试")
})

```

The annotation builds a cache key using SpEL expressions, stores a counter in Redis, and automatically rejects requests that exceed the defined `rate` within the `interval`.

### Applying @RateLimiter to Custom Endpoints

To implement frequency-based idempotency:

1. **Define a cache constant** in [`CacheConstants.java`](https://github.com/continew-org/continew-admin/blob/main/CacheConstants.java) for your operation prefix.
   ```java
   public static final String ORDER_SUBMIT_PREFIX = "order:submit:";
   ```

2. **Annotate the controller method** with operation-specific parameters.
   ```java
   @RateLimiter(name = CacheConstants.ORDER_SUBMIT_PREFIX,
                key = "#request.orderId",
                rate = 1, interval = 5, unit = TimeUnit.MINUTES,
                message = "该订单已在处理中，请稍后再试")
   public ResponseEntity<Void> submitOrder(@RequestBody OrderRequest request) { … }
   ```

3. The underlying `RedisCache` implementation increments a counter; when the count exceeds `rate`, it throws a `ServiceException` with your custom message.

## Choosing Between Lock-Based and Rate-Based Idempotency

Select the appropriate pattern based on your consistency requirements:

| Requirement | Recommended Pattern | Implementation |
|-------------|---------------------|----------------|
| **Exactly-once semantics** (prevent duplicate records, files, or events) | **RedisLockUtils** | Guarantees only one thread/process proceeds globally |
| **Frequency throttling** (prevent abuse, spam, or burst traffic) | **@RateLimiter** | Simple counter per key with auto-expiration |
| **Stateless clients** (mobile apps, third-party integrations) | Both patterns work | Locks require explicit handling; rate-limiter uses only cache keys |
| **Audit trail persistence** (track request identifiers) | **Combined approach** | Store `request-id` in Redis using `SETNX` alongside the lock |

## Complete Implementation Examples

### Idempotent Order Creation with Redis Lock

```java
@RestController
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    @PostMapping("/api/orders")
    public ResultVO create(@RequestBody OrderCreateReq req) {
        // Unique key per user + order number
        String lockKey = StrUtil.format("Lock:{}:order:{}", req.getUserId(), req.getOrderNo());

        try (RedisLockUtils lock = RedisLockUtils.tryLock(lockKey)) {
            if (!lock.isLocked()) {
                return ResultVO.fail("Order submission is already in progress, please do not repeat.");
            }
            
            // Critical section - executes exactly once
            orderService.createOrder(req);
            return ResultVO.success();
        }
    }
}

```

### Idempotent File Upload Endpoint

```java
@PostMapping("/api/files/upload")
public ResultVO upload(@RequestParam MultipartFile file,
                       @RequestParam String parentPath,
                       @RequestParam String storageCode) {

    // Reuse the same strategy as FileServiceImpl#createParentDir
    String lockKey = StrUtil.format("Lock:{}:upload:{}", storageCode, parentPath);
    
    try (RedisLockUtils lock = RedisLockUtils.tryLock(lockKey)) {
        if (!lock.isLocked()) {
            return ResultVO.fail("File is already being uploaded to this directory.");
        }
        fileService.upload(file, parentPath, storageCode);
        return ResultVO.success();
    }
}

```

### Rate-Limited Password Reset Request

```java
@PostMapping("/api/password/reset")
@RateLimiter(name = CacheConstants.PASSWORD_RESET_KEY_PREFIX,
            key = "#email",
            rate = 3, interval = 1, unit = TimeUnit.HOURS,
            message = "密码重置请求已达上限，请稍后再试")
public ResultVO reset(@RequestParam String email) {
    // send reset email …
    return ResultVO.success();
}

```

## Key Source Files

- **[`FileServiceImpl.java`](https://github.com/continew-org/continew-admin/blob/main/FileServiceImpl.java)** (`continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java#L317-L329`): Demonstrates distributed lock acquisition and release patterns for filesystem operations.
- **[`CaptchaController.java`](https://github.com/continew-org/continew-admin/blob/main/CaptchaController.java)** (`continew-server/src/main/java/top/continew/admin/controller/CaptchaController.java#L150-L156`): Shows declarative rate-limiting using SpEL expressions for dynamic key generation.
- **[`CacheConstants.java`](https://github.com/continew-org/continew-admin/blob/main/CacheConstants.java)** ([`continew-common/src/main/java/top/continew/admin/common/constant/CacheConstants.java`](https://github.com/continew-org/continew-admin/blob/main/continew-common/src/main/java/top/continew/admin/common/constant/CacheConstants.java)): Central repository for all Redis key prefixes used in idempotency checks.
- **[`RedisLockUtils.java`](https://github.com/continew-org/continew-admin/blob/main/RedisLockUtils.java)**: Provided by the `continew-starter-cache-redisson` module, wrapping Redisson's `RLock` with convenient try-with-resources semantics.

## Summary

- **Use `RedisLockUtils`** when you require exactly-once execution semantics for mutating operations like order creation or file processing.
- **Use `@RateLimiter`** when you need to throttle request frequency rather than enforce absolute uniqueness, such as for SMS or captcha endpoints.
- Both utilities rely on **Redis** for distributed coordination, ensuring consistency across horizontally scaled ContiNew-Admin instances.
- Always construct **deterministic lock keys** using user identifiers and business keys to prevent cross-contamination between different resources.

## Frequently Asked Questions

### What is the default TTL for RedisLockUtils and how do I change it?

The default TTL is **30 seconds**. Pass a custom `Duration` as the second argument to `tryLock()` for longer operations: `RedisLockUtils.tryLock(lockKey, Duration.ofMinutes(5))`.

### Can I use both @RateLimiter and RedisLockUtils together?

Yes. Use `@RateLimiter` to prevent abuse (e.g., maximum 3 attempts per hour) and `RedisLockUtils` to ensure the actual business logic executes only once. This layered approach provides both frequency control and exactly-once semantics.

### How should I generate unique lock keys for distributed microservices?

Include the **service name**, **user identifier**, and **resource identifier** in the key format: `StrUtil.format("Lock:{}:{}:{}", serviceName, userId, resourceId)`. This prevents collisions when multiple services share the same Redis instance.

### What happens if the Redis server becomes unavailable during idempotency checks?

Both patterns depend on Redis availability. If Redis is down, `RedisLockUtils.tryLock()` will fail to acquire the lock (returning `isLocked() == false`), and `@RateLimiter` will throw a cache connection exception. Implement fallback logic or circuit breakers for critical paths when Redis connectivity is unreliable.