Backend Development 17 min read

Implementing Interface Debounce with Distributed Locks in Java Backend Systems

This article explains the concept of request debouncing, identifies which API endpoints need it, and demonstrates two distributed lock solutions—shared Redis cache and Redisson—by providing complete Java code examples, configuration steps, and test results to prevent duplicate submissions in backend services.

Top Architect
Top Architect
Top Architect
Implementing Interface Debounce with Distributed Locks in Java Backend Systems

As a seasoned Java backend developer, the author shares practical techniques for preventing duplicate requests (debounce) in web APIs, emphasizing both client‑side and server‑side safeguards.

What is debounce? It protects against accidental multiple clicks and network jitter that can cause repeated submissions, which may create duplicate data records.

An ideal debounce component should be logically correct, fast, easy to integrate, and provide clear user feedback.

Which interfaces need debounce?

User input interfaces such as search boxes and form fields.

Button click interfaces like form submissions and settings saves.

Scroll‑loading interfaces such as infinite scroll or pull‑to‑refresh.

How to determine duplicate requests?

Use a time interval threshold, compare key parameters, and optionally compare request URLs.

Distributed deployment solutions

1. Shared Redis cache

Workflow diagram (omitted) shows using a Redis key with SET_IF_ABSENT to ensure only the first request proceeds.

2. Distributed lock with Redisson

Workflow diagram (omitted) demonstrates acquiring a Redisson lock, setting an expiration, and releasing it after processing.

Code implementations

Controller method:

@PostMapping("/add")
@RequiresPermissions(value = "add")
@Log(methodDesc = "添加用户")
public ResponseEntity
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;
    /** 角色ID列表 */
    private List
roleIdList;
}

Key generator utility:

public class RequestKeyGenerator {
    public static String getLockKey(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        RequestLock requestLock = method.getAnnotation(RequestLock.class);
        Object[] args = joinPoint.getArgs();
        Parameter[] parameters = method.getParameters();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < parameters.length; i++) {
            RequestKeyParam keyParam = parameters[i].getAnnotation(RequestKeyParam.class);
            if (keyParam == null) continue;
            sb.append(requestLock.delimiter()).append(args[i]);
        }
        if (StringUtils.isEmpty(sb.toString())) {
            Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            for (int i = 0; i < parameterAnnotations.length; i++) {
                Object object = args[i];
                Field[] fields = object.getClass().getDeclaredFields();
                for (Field field : fields) {
                    RequestKeyParam annotation = field.getAnnotation(RequestKeyParam.class);
                    if (annotation == null) continue;
                    field.setAccessible(true);
                    sb.append(requestLock.delimiter()).append(ReflectionUtils.getField(field, object));
                }
            }
        }
        return requestLock.prefix() + sb;
    }
}

Redis lock aspect:

@Aspect
@Configuration
@Order(2)
public class RedisRequestLockAspect {
    private final StringRedisTemplate stringRedisTemplate;
    @Autowired
    public RedisRequestLockAspect(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Around("execution(public * * (..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
    public Object interceptor(ProceedingJoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        RequestLock requestLock = method.getAnnotation(RequestLock.class);
        if (StringUtils.isEmpty(requestLock.prefix())) {
            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "重复提交前缀不能为空");
        }
        String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
        Boolean success = stringRedisTemplate.execute((RedisCallback
) 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, "您的操作太快了,请稍后重试");
        }
        try {
            return joinPoint.proceed();
        } catch (Throwable t) {
            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系统异常");
        }
    }
}

Redisson configuration:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.10.6</version>
</dependency>
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

Redisson lock aspect:

@Aspect
@Configuration
@Order(2)
public class RedissonRequestLockAspect {
    private RedissonClient redissonClient;
    @Autowired
    public RedissonRequestLockAspect(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }
    @Around("execution(public * * (..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
    public Object interceptor(ProceedingJoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        RequestLock requestLock = method.getAnnotation(RequestLock.class);
        if (StringUtils.isEmpty(requestLock.prefix())) {
            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "重复提交前缀不能为空");
        }
        String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
        RLock lock = redissonClient.getLock(lockKey);
        boolean isLocked = false;
        try {
            isLocked = lock.tryLock();
            if (!isLocked) {
                throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
            }
            lock.lock(requestLock.expire(), requestLock.timeUnit());
            return joinPoint.proceed();
        } catch (Throwable t) {
            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系统异常");
        } finally {
            if (isLocked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

Annotation for key parameters:

@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestKeyParam {}

Testing results show the first submission succeeds, rapid duplicate submissions are blocked with a "操作太快" error, and after the lock expires the request succeeds again, confirming the debounce works but noting that true idempotency also requires database constraints.

In summary, using a shared Redis key or Redisson distributed lock effectively prevents duplicate API calls, but developers should also enforce unique constraints at the database level and consider additional request attributes (e.g., user ID, IP) when generating lock keys.

BackendJavaRedisSpringDistributed LockIdempotencyDebounce
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.