Spring Boot + Redis: Automatic API Idempotency for Duplicate Request Prevention
This tutorial demonstrates how to implement automatic idempotency in Spring Boot using Redis, a custom @AutoIdempotent annotation, token-based validation, and a HandlerInterceptor to prevent duplicate submissions in high-volume APIs.
The article addresses the problem of duplicate submissions in externally exposed APIs and explains idempotency: multiple executions must have the same effect as a single execution. It reviews common approaches — unique database indexes, token mechanisms, pessimistic/optimistic locking, and pre-check queries — before presenting a Redis-based automated solution.
Redis Service Layer
A RedisService component wraps RedisTemplate with methods for set, setEx (with TTL), exists, get, and remove. The setEx method stores a key-value pair and sets an expiration (e.g., 10,000 seconds) to avoid data redundancy.
@Component
public class RedisService {
@Autowired
private RedisTemplate redisTemplate;
public boolean set(final String key, Object value) { ... }
public boolean setEx(final String key, Object value, Long expireTime) { ... }
public boolean exists(final String key) { return redisTemplate.hasKey(key); }
public Object get(final String key) { ... }
public boolean remove(final String key) { ... }
}Custom Annotation
The @AutoIdempotent annotation marks methods that require automatic idempotency. It uses @Target(ElementType.METHOD) and @Retention(RetentionPolicy.RUNTIME) so the interceptor can detect it via reflection at runtime.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {}Token Service
TokenServicedefines two operations: createToken() generates a random UUID, prefixes it with a constant (e.g., TOKEN_PREFIX), stores it in Redis with a 10,000-second TTL via redisService.setEx, and returns the token string. checkToken(HttpServletRequest) extracts the token from the token header (falling back to request parameter), verifies its existence in Redis via redisService.exists, deletes it with redisService.remove to enforce single-use, and throws ServiceException with specific codes (100 for missing token, 200 for duplicate) if validation fails.
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private RedisService redisService;
public String createToken() {
String str = RandomUtil.randomUUID();
StrBuilder token = new StrBuilder();
token.append(Constant.Redis.TOKEN_PREFIX).append(str);
redisService.setEx(token.toString(), token.toString(), 10000L);
return StrUtil.isNotEmpty(token.toString()) ? token.toString() : null;
}
public boolean checkToken(HttpServletRequest request) throws Exception {
String token = request.getHeader(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {
token = request.getParameter(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {
throw new ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
}
}
if (!redisService.exists(token)) {
throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
boolean remove = redisService.remove(token);
if (!remove) {
throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
return true;
}
}Interceptor Configuration
WebConfigurationimplements WebMvcConfigurerAdapter and registers AutoIdempotentInterceptor via addInterceptors. The interceptor implements HandlerInterceptor; its preHandle method checks if the handler is a HandlerMethod, retrieves the method, and looks for @AutoIdempotent. If present, it calls tokenService.checkToken(request). On success, it returns true to proceed; on exception, it writes a JSON error response via writeReturnJson and re-throws the exception.
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) return true;
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
try {
return tokenService.checkToken(request);
} catch (Exception ex) {
ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
throw ex;
}
}
return true;
}
// postHandle, afterCompletion, writeReturnJson omitted for brevity
}Test Demonstration
A BusinessController exposes POST /get/token to obtain a token and POST /test/Idempotence annotated with @AutoIdempotent. Using Postman, the first request with the token in the header succeeds; the second request with the same token returns a "repetitive operation" error (code 200), confirming the idempotency check works.
@RestController
public class BusinessController {
@Resource
private TokenService tokenService;
@PostMapping("/get/token")
public String getToken() {
String token = tokenService.createToken();
if (StrUtil.isNotEmpty(token)) {
ResultVo resultVo = new ResultVo();
resultVo.setCode(Constant.code_success);
resultVo.setMessage(Constant.SUCCESS);
resultVo.setData(token);
return JSONUtil.toJsonStr(resultVo);
}
return StrUtil.EMPTY;
}
@AutoIdempotent
@PostMapping("/test/Idempotence")
public String testIdempotence() {
String businessResult = testService.testIdempotence();
if (StrUtil.isNotEmpty(businessResult)) {
return JSONUtil.toJsonStr(ResultVo.getSuccessResult(businessResult));
}
return StrUtil.EMPTY;
}
}Summary
The article concludes that this automated approach — combining Spring Boot interceptors, a custom annotation, Redis-backed tokens, and single-use deletion — provides an elegant, scalable way to guarantee API idempotency, preventing dirty data and reducing concurrent load compared to manual per-method checks.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
