Implementing Interface Debounce and Distributed Locks in Java Backend Applications

This article explains how to prevent duplicate API submissions by applying debounce principles and implementing distributed locks using Redis and Redisson in a Java Spring backend, providing detailed code examples, key generation strategies, and practical testing results.

Top Architect
Top Architect
Top Architect
Implementing Interface Debounce and Distributed Locks in Java Backend Applications

The author, a senior Java backend architect, shares practical techniques for preventing duplicate submissions (debounce) of API requests.

What is debounce? It protects against user accidental multiple clicks and network retries; while the frontend can show loading states, the backend must also enforce debounce logic to avoid processing the same request repeatedly.

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

Which interfaces need debounce? User input interfaces (e.g., search boxes), button click interfaces (e.g., form submissions), and scroll‑loading interfaces (e.g., infinite scroll) are typical candidates.

To determine if two requests are duplicates, compare a time interval, key parameters with strong identifiers, and optionally the request URL.

Distributed deployment solutions: Two common approaches are using a shared cache (Redis) or a distributed lock (Redisson), each illustrated with flow diagrams.

Implementation example – user‑add API:

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

Request DTO:

package com.summo.demo.model.request;<br/>import java.util.List;<br/>import lombok.Data;<br/>@Data<br/>public class AddReq {<br/>    /** 用户名称 */ private String userName;<br/>    /** 用户手机号 */ private String userPhone;<br/>    /** 角色ID列表 */ private List<Long> roleIdList;<br/>}

Define a custom annotation to mark methods that require request locking:

@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})<br/>@Retention(RetentionPolicy.RUNTIME)<br/>@Documented<br/>@Inherited<br/>public @interface RequestLock {<br/>    String prefix() default "reqLock:";<br/>    long expire() default 5;<br/>    TimeUnit timeUnit() default TimeUnit.SECONDS;<br/>    String delimiter() default "&";<br/>}

Generate a unique lock key based on annotated parameters:

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

Redis‑based lock aspect:

@Aspect<br/>@Configuration<br/>@Order(2)<br/>public class RedisRequestLockAspect {<br/>    private final StringRedisTemplate stringRedisTemplate;<br/>    @Autowired<br/>    public RedisRequestLockAspect(StringRedisTemplate stringRedisTemplate) {<br/>        this.stringRedisTemplate = stringRedisTemplate;<br/>    }<br/>    @Around("execution(public * * (..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")<br/>    public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {<br/>        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();<br/>        RequestLock requestLock = method.getAnnotation(RequestLock.class);<br/>        String lockKey = RequestKeyGenerator.getLockKey(joinPoint);<br/>        Boolean success = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -><br/>            connection.set(lockKey.getBytes(), new byte[0],<br/>                Expiration.from(requestLock.expire(), requestLock.timeUnit()),<br/>                RedisStringCommands.SetOption.SET_IF_ABSENT));<br/>        if (!success) {<br/>            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");<br/>        }<br/>        try {<br/>            return joinPoint.proceed();<br/>        } finally {<br/>            stringRedisTemplate.delete(lockKey);<br/>        }<br/>    }<br/>}

Redisson‑based lock aspect and configuration:

@Configuration<br/>public class RedissonConfig {<br/>    @Bean<br/>    public RedissonClient redissonClient() {<br/>        Config config = new Config();<br/>        config.useSingleServer().setAddress("redis://127.0.0.1:6379");<br/>        return Redisson.create(config);<br/>    }<br/>}
@Aspect<br/>@Configuration<br/>@Order(2)<br/>public class RedissonRequestLockAspect {<br/>    private final RedissonClient redissonClient;<br/>    @Autowired<br/>    public RedissonRequestLockAspect(RedissonClient redissonClient) {<br/>        this.redissonClient = redissonClient;<br/>    }<br/>    @Around("execution(public * * (..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")<br/>    public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {<br/>        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();<br/>        RequestLock requestLock = method.getAnnotation(RequestLock.class);<br/>        String lockKey = RequestKeyGenerator.getLockKey(joinPoint);<br/>        RLock lock = redissonClient.getLock(lockKey);<br/>        boolean locked = lock.tryLock();<br/>        if (!locked) {<br/>            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");<br/>        }<br/>        lock.lock(requestLock.expire(), requestLock.timeUnit());<br/>        try {<br/>            return joinPoint.proceed();<br/>        } finally {<br/>            if (lock.isHeldByCurrentThread()) {<br/>                lock.unlock();<br/>            }<br/>        }<br/>    }<br/>}

Testing shows the first submission succeeds, rapid duplicate submissions are blocked with an error, and after the lock expires the request succeeds again, confirming the debounce works. However, true idempotency also requires business‑level checks such as unique constraints in the database.

In summary, combining request‑level debouncing with Redis or Redisson distributed locks provides an effective safeguard against duplicate API calls in distributed Java backend systems.

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.

Javaredisspringdistributed-lockredissonDebounce
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.