Achieve Automatic API Idempotency with Spring Boot, Redis, and Custom Annotations

This article explains how to implement automatic idempotency for Spring Boot APIs using Redis, a custom @AutoIdempotent annotation, token generation and verification, and an interceptor, providing step‑by‑step code examples and a testing workflow to prevent duplicate operations.

Programmer DD
Programmer DD
Programmer DD
Achieve Automatic API Idempotency with Spring Boot, Redis, and Custom Annotations

Preface

In real development, an exposed API may receive many requests. Idempotency means that executing the operation any number of times has the same effect as executing it once, ensuring that database changes occur only once.

Create a unique index in the database to guarantee a single row insertion.

Use a token mechanism: obtain a token before each request and include it in the request header; the server validates and deletes the token after use.

Apply pessimistic or optimistic locks to prevent concurrent updates.

Perform a check‑then‑act pattern: query first, if data exists reject the request; otherwise allow it.

Redis implementation diagram:

Set up Redis service API

Install and start a Redis server.

Import Spring Boot's Redis starter (or Jedis) and use RedisTemplate for set and exists operations.

@Component
public class RedisService {
    @Autowired
    private RedisTemplate redisTemplate;

    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    public boolean setEx(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    public Object get(final String key) {
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        return operations.get(key);
    }

    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;
    }
}

Custom annotation AutoIdempotent

Define an annotation that can be placed on methods to enable automatic idempotency. It is retained at runtime and applicable only to methods.

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {}

Token creation and verification

Token service interface with methods to create a token and to check a token.

public interface TokenService {
    /**
     * Create token
     */
    public String createToken();

    /**
     * Verify token
     */
    public boolean checkToken(HttpServletRequest request) throws Exception;
}

Implementation stores a UUID token in Redis with an expiration time, retrieves the token from request header or parameter, throws exceptions for missing or repeated tokens, and removes the token after successful verification.

@Service
public class TokenServiceImpl implements TokenService {
    @Autowired
    private RedisService redisService;

    @Override
    public String createToken() {
        String str = RandomUtil.randomUUID();
        StrBuilder token = new StrBuilder();
        try {
            token.append(Constant.Redis.TOKEN_PREFIX).append(str);
            redisService.setEx(token.toString(), token.toString(), 10000L);
            if (StrUtil.isNotEmpty(token.toString())) {
                return token.toString();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    @Override
    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

Web configuration class adds AutoIdempotentInterceptor to the MVC interceptor registry.

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
    @Resource
    private AutoIdempotentInterceptor autoIdempotentInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry);
    }
}

Interceptor scans for @AutoIdempotent on methods, invokes tokenService.checkToken, and returns a JSON error response when validation fails.

@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {
    @Autowired
    private TokenService tokenService;

    @Override
    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;
    }

    private void writeReturnJson(HttpServletResponse response, String json) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
            writer.print(json);
        } catch (IOException e) {
        } finally {
            if (writer != null) writer.close();
        }
    }
}

Test case

BusinessController provides /get/token to obtain a token and /test/Idempotence annotated with @AutoIdempotent. Using Postman, the first call succeeds; subsequent calls are rejected as duplicate operations.

@RestController
public class BusinessController {
    @Resource
    private TokenService tokenService;
    @Resource
    private TestService testService;

    @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)) {
            ResultVo successResult = ResultVo.getSuccessResult(businessResult);
            return JSONUtil.toJsonStr(successResult);
        }
        return StrUtil.EMPTY;
    }
}

Conclusion

This tutorial demonstrates an elegant way to achieve API idempotency using Spring Boot, Redis, a custom annotation, and an interceptor, which prevents duplicate data processing, reduces unnecessary concurrency, and improves system scalability.

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 BootInterceptorIdempotency
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.