How to Prevent Duplicate Submissions in Java APIs with Redis and Redisson

This article explains the concept of debounce for backend APIs, identifies which endpoints need it, and provides two distributed solutions—shared Redis cache and Redisson lock—complete with annotations, key generation logic, code examples, testing results, and tips for achieving true idempotency.

Top Architect
Top Architect
Top Architect
How to Prevent Duplicate Submissions in Java APIs with Redis and Redisson

Preface

As an experienced Java backend developer, I have built many management back‑ends and mini‑programs. I have never suffered data loss due to code crashes because I follow strict coding standards, use simple business logic and accumulated practical techniques.

What is Debounce?

Debounce means preventing user “hand shaking” and network jitter. In web systems, repeated form submissions caused by accidental clicks or network delays can create duplicate records. Front‑end usually shows a loading state, but back‑end must also implement debounce logic to reject duplicate requests.

An ideal debounce component should be:

Logically correct, without false positives.

Fast response.

Easy to integrate and decoupled from business logic.

Provide clear user feedback, e.g., “You are clicking too fast”.

Thought Process

Not every API needs debounce. Typically required for:

User‑input APIs such as search or form fields.

Button‑click APIs like submit or save.

Scroll‑load APIs such as pull‑to‑refresh or infinite scroll.

How to Determine Duplicate Requests?

Duplicate detection can be based on a time window, comparison of key parameters, and optionally the request URL.

Debounce in Distributed Deployment

Solution 1: Shared Cache

Use a shared Redis key with an expiration time. The flow diagram is shown below.

Shared cache diagram
Shared cache diagram

Solution 2: Distributed Lock

Use Redis SET_IF_ABSENT or Redisson lock to acquire a lock per request. The flow diagram is shown below.

Distributed lock diagram
Distributed lock diagram
Common distributed components are Redis and Zookeeper, but Redis is usually preferred because it is already part of most web stacks.

Implementation

Example controller method:

@PostMapping("/add")
@RequiresPermissions("add")
@Log(methodDesc="添加用户")
public ResponseEntity<String> add(@RequestBody AddReq addReq) {
    return userService.add(addReq);
}

Request DTO:

package com.summo.demo.model.request;
import java.util.List;
import lombok.Data;
@Data
public class AddReq {
    private String userName;
    private String userPhone;
    private List<Long> roleIdList;
}
Without a unique index on userPhone, each call creates a new user even if the phone number is the same.

RequestLock Annotation

Define @RequestLock with prefix, expire, timeUnit, delimiter, etc., and place it on the controller method.

Generating a Unique Key

Use @RequestKeyParam on method parameters or fields to compose the lock key. The generator reflects annotated fields and builds a key like “prefix&userName&userPhone”.

public class RequestKeyGenerator {
    public static String getLockKey(ProceedingJoinPoint joinPoint) {
        // ... (logic omitted for brevity)
        return requestLock.prefix() + sb;
    }
}

Redis Cache Implementation

Aspect intercepts the method, builds the lock key, and executes SET_IF_ABSENT with expiration via StringRedisTemplate.

@Around("execution(public * * (..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
public Object interceptor(ProceedingJoinPoint joinPoint) {
    // ... (logic omitted)
    Boolean success = stringRedisTemplate.execute(connection ->
        connection.set(lockKey.getBytes(), new byte[0],
            Expiration.from(requestLock.expire(), requestLock.timeUnit()),
            RedisStringCommands.SetOption.SET_IF_ABSENT));
    if (!success) {
        throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "Your operation is too fast, please try later");
    }
    return joinPoint.proceed();
}

Redisson Distributed Lock

Similar aspect uses RedissonClient to acquire a lock, set expiration, execute the method, and finally release the lock.

@Around("execution(public * * (..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
public Object interceptor(ProceedingJoinPoint joinPoint) {
    // ... (logic omitted)
    RLock lock = redissonClient.getLock(lockKey);
    if (!lock.tryLock()) {
        throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "Your operation is too fast, please try later");
    }
    lock.lock(requestLock.expire(), requestLock.timeUnit());
    try {
        return joinPoint.proceed();
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

Testing Results

First submission: “Add user success”.

Rapid repeat: “BIZ-0001: Your operation is too fast, please try later”.

After lock expires: “Add user success”.

Debounce works, but once the cache expires or the lock is released, duplicate requests can still occur. True idempotency also requires business‑level checks and database unique constraints.
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend Developmentdistributed-lockIdempotency
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

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.