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.

Architect's Guide
Architect's Guide
Architect's Guide
Spring Boot + Redis: Automatic API Idempotency for Duplicate Request Prevention

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

TokenService

defines 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

WebConfiguration

implements 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.

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.

JavaRedisSpring BootinterceptorIdempotencyannotationtokenduplicate-submission
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.