Implementing Interface Debounce and Request Lock in Java Backend with Redis and Redisson
This article explains the concept of interface debouncing, identifies which API endpoints need it, and provides detailed Java implementations using custom @RequestLock annotations, Redis caching, and Redisson distributed locks to prevent duplicate submissions in backend services.
As an experienced Java backend developer, the author shares practical techniques for preventing duplicate submissions (debounce) in web APIs, drawing on years of experience with multi‑tenant systems, message centers, and open‑platform integrations.
What is debounce? Debounce protects against both user‑induced rapid clicks and network‑induced request retries. In web systems, uncontrolled form submissions can create duplicate records, so both frontend loading states and backend debounce logic are required.
An ideal debounce component should be logically correct, fast, easy to integrate, decoupled from business logic, and provide clear user feedback such as “you are clicking too fast”.
Which interfaces need debounce?
User‑input interfaces (e.g., search boxes, form fields) where requests fire frequently but can be delayed until input stabilises.
Button‑click interfaces (e.g., submit, save) where rapid clicks should be throttled.
Scroll‑loading interfaces (e.g., infinite scroll, pull‑to‑refresh) where requests are triggered often during scrolling.
How to determine duplicate requests?
Duplicate detection relies on a time window and a comparison of key parameters (not necessarily all parameters). Optionally, the request URL can also be compared.
Distributed deployment solutions
Two common approaches are presented:
Shared cache (Redis) – store a lock key with an expiration; if the key already exists, reject the request.
Distributed lock (Redisson) – acquire a lock, set an expiration, and release it after processing.
Implementation details
Custom annotation @RequestLock is defined to mark methods that require debounce. The annotation includes prefix, expiration, time unit, and a delimiter for key construction.
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestLock {
String prefix() default "";
long expire() default 5L;
TimeUnit timeUnit() default TimeUnit.SECONDS;
String delimiter() default "&";
}Key generation is handled by RequestKeyGenerator , which inspects method parameters and fields annotated with @RequestKeyParam to build a unique lock key.
public static String getLockKey(ProceedingJoinPoint joinPoint) {
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
Method method = ms.getMethod();
RequestLock lock = method.getAnnotation(RequestLock.class);
Object[] args = joinPoint.getArgs();
Parameter[] params = method.getParameters();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < params.length; i++) {
RequestKeyParam kp = params[i].getAnnotation(RequestKeyParam.class);
if (kp != null) {
sb.append(lock.delimiter()).append(args[i]);
}
}
if (sb.length() == 0) {
Annotation[][] paramAnnos = method.getParameterAnnotations();
for (int i = 0; i < paramAnnos.length; i++) {
Object obj = args[i];
for (Field field : obj.getClass().getDeclaredFields()) {
if (field.getAnnotation(RequestKeyParam.class) != null) {
field.setAccessible(true);
sb.append(lock.delimiter()).append(ReflectionUtils.getField(field, obj));
}
}
}
}
return lock.prefix() + sb.toString();
}Redis implementation uses StringRedisTemplate to execute a SET command with SET_IF_ABSENT and an expiration. If the command fails, a BizException with a “operation too fast” message is thrown.
@Around("execution(public * *(..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
public Object interceptor(ProceedingJoinPoint jp) {
Method method = ((MethodSignature) jp.getSignature()).getMethod();
RequestLock lock = method.getAnnotation(RequestLock.class);
String lockKey = RequestKeyGenerator.getLockKey(jp);
Boolean success = stringRedisTemplate.execute(conn ->
conn.set(lockKey.getBytes(), new byte[0],
Expiration.from(lock.expire(), lock.timeUnit()),
RedisStringCommands.SetOption.SET_IF_ABSENT));
if (!success) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "Your operation is too fast, please try later");
}
try { return jp.proceed(); }
catch (Throwable t) { throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "System error"); }
}Redisson implementation obtains an RLock from a RedissonClient , tries to acquire it, sets the same expiration, proceeds with the method, and finally releases the lock.
@Around("execution(public * *(..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
public Object interceptor(ProceedingJoinPoint jp) {
Method method = ((MethodSignature) jp.getSignature()).getMethod();
RequestLock lock = method.getAnnotation(RequestLock.class);
String lockKey = RequestKeyGenerator.getLockKey(jp);
RLock rLock = redissonClient.getLock(lockKey);
boolean acquired = false;
try {
acquired = rLock.tryLock();
if (!acquired) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "Your operation is too fast, please try later");
}
rLock.lock(lock.expire(), lock.timeUnit());
return jp.proceed();
} catch (Throwable t) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "System error");
} finally {
if (acquired && rLock.isHeldByCurrentThread()) {
rLock.unlock();
}
}
}Configuration for Redisson is straightforward: create a RedissonClient bean pointing to the Redis server.
@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);
}
}Testing shows that the first submission succeeds, rapid repeated submissions are blocked with a “BIZ-0001: Your operation is too fast” error, and after the lock expires the request succeeds again. The author notes that true idempotency also requires database unique constraints and business‑level checks.
Finally, the article ends with a call to join the author’s community and a list of additional resources, but the technical core remains the debounce and request‑lock implementations for Java backend services.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.