Backend Development 10 min read

Implementing Idempotent Repeat Submission Prevention in Spring Boot with AOP and Redis

This article explains how to prevent duplicate data submissions in a Spring Boot application by designing an idempotent mechanism using AOP and Redis, covering the concept, design strategy, custom annotation, aspect implementation, and a simple test controller.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Implementing Idempotent Repeat Submission Prevention in Spring Boot with AOP and Redis

Idempotent repeat submission refers to preventing duplicate data submissions that could cause dirty data or business chaos; it is a low‑probability event distinct from concurrency stress testing.

The goal is to design a system that, after a successful request, restricts the same data from being submitted again within a configurable time window, allowing rapid release of the restriction on failure or exception.

Design strategy : generate a unique ID from the user’s request path, parameters, and token, store it in Redis, and follow these steps:

Intercept the request via an AOP advice, extract the URL, parameters, and token, and compute a unique ID.

Check whether the ID already exists in Redis and whether it is still valid.

If the ID does not exist, proceed with normal business logic; otherwise, throw an exception indicating a duplicate submission.

After method execution, if the result is successful, keep the Redis key; if the business fails, delete the key so that the next submission can pass.

Custom annotation @RepeatSubmit is defined to mark methods that require idempotent protection. It includes three attributes:

interval : the minimum interval time (ms) between two submissions.

timeUnit : the time unit, default milliseconds.

message : an internationalized warning message.

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/** Interval time (ms) */
int interval() default 5000;
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
/** Internationalized message */
String message() default "{repeat.submit.message}";
}

Custom aspect @RepeatSubmitAspect implements the idempotent logic with three advices:

doBefore : a @Before advice that extracts the interval, request URL, token, and parameters, builds a submitKey (MD5 of token + parameters), constructs a Redis key, and attempts to set it with an expiration; if the key already exists, it throws a ServiceException with the configured message.

doAfterReturning : an @AfterReturning advice that receives the method’s return object, checks if it is an instance of R , and if the response code indicates failure, deletes the Redis key and clears the thread‑local cache.

doAfterThrowing : an @AfterThrowing advice that removes the Redis key and thread‑local entry when an exception occurs.

@Aspect
@Component
public class RepeatSubmitAspect {
private static final ThreadLocal
KEY_CACHE = new ThreadLocal<>();
@Before("@annotation(repeatSubmit)")
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
if (interval < 1000) {
throw new ServiceException("Repeat interval cannot be less than 1 second");
}
HttpServletRequest request = ServletUtils.getRequest();
String nowParams = argsArrayToString(point.getArgs());
String url = request.getRequestURI();
String submitKey = StringUtils.trimToEmpty(request.getHeader(SaManager.getConfig().getTokenName()));
submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
KEY_CACHE.set(cacheRepeatKey);
} else {
String message = repeatSubmit.message();
if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {
message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));
}
throw new ServiceException(message);
}
}
@AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
if (jsonResult instanceof R) {
try {
R
r = (R
) jsonResult;
if (r.getCode() == R.SUCCESS) {
return;
}
RedisUtils.deleteObject(KEY_CACHE.get());
} finally {
KEY_CACHE.remove();
}
}
}
@AfterThrowing(value = "@annotation(repeatSubmit)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Exception e) {
RedisUtils.deleteObject(KEY_CACHE.get());
KEY_CACHE.remove();
}
private String argsArrayToString(Object[] paramsArray) {
StringJoiner params = new StringJoiner(" ");
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
}
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
params.add(JsonUtils.toJsonString(o));
}
}
return params.toString();
}
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class
clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.values()) {
return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse || o instanceof BindingResult;
}
}

A simple test controller demonstrates the usage:

@RestController
@RequestMapping("/repeat")
@SaIgnore
public class RepeatController {
@PostMapping
@RepeatSubmit(interval = 2000)
public R
repeat1(String info) {
log.info("Request succeeded, info: " + info);
return R.ok("Request succeeded");
}
}

Using Apifox, sending two requests within 2 seconds triggers the duplicate‑submission exception, confirming that the AOP + Redis solution successfully enforces idempotency.

Thus, the article provides a complete guide to designing and implementing an idempotent repeat‑submission mechanism in a Spring Boot backend.

backendJavaAOPRedisIdempotencySpringBoot
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.