Implementing Automatic Idempotency in Spring Boot with Redis and Interceptors

This tutorial explains the concept of idempotency in web APIs and demonstrates a complete Spring Boot solution using Redis, custom annotations, token validation, and interceptor configuration to ensure that each request modifies the database only once, even under repeated calls.

Programmer DD
Programmer DD
Programmer DD
Implementing Automatic Idempotency in Spring Boot with Redis and Interceptors

Introduction

In real development projects, 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.

Common Idempotency Techniques

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 backend validates and deletes the token after use.

Apply pessimistic or optimistic locks; pessimistic locks prevent other SQL statements from updating the row during a SELECT ... FOR UPDATE when using InnoDB.

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

Redis Service API

First, set up a Redis server and include the Spring Boot starter or Jedis. The following utility class provides basic Redis operations.

/** redis工具类 */
@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 requiring idempotency. The interceptor will detect this annotation and invoke token validation.

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

Token Service Interface and Implementation

The token service creates a random UUID token, stores it in Redis with an expiration time, and validates incoming tokens.

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

    /** 检验 token */
    boolean checkToken(HttpServletRequest request) throws Exception;
}
@Service
public class TokenServiceImpl implements TokenService {
    @Autowired
    private RedisService redisService;

    @Override
    public String createToken() {
        String uuid = RandomUtil.randomUUID();
        String token = Constant.Redis.TOKEN_PREFIX + uuid;
        try {
            redisService.setEx(token, token, 10000L);
            if (StrUtil.isNotEmpty(token)) {
                return token;
            }
        } 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 removed = redisService.remove(token);
        if (!removed) {
            throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
        }
        return true;
    }
}

Web Configuration

Add the AutoIdempotentInterceptor to the Spring MVC interceptor chain.

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

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

Interceptor Implementation

The interceptor checks for the @AutoIdempotent annotation and calls TokenService.checkToken. If validation fails, it returns a JSON error response.

@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;
        }
        Method method = ((HandlerMethod) handler).getMethod();
        AutoIdempotent annotation = method.getAnnotation(AutoIdempotent.class);
        if (annotation != null) {
            try {
                return tokenService.checkToken(request);
            } catch (Exception ex) {
                ResultVo failed = ResultVo.getFailedResult(101, ex.getMessage());
                writeReturnJson(response, JSONUtil.toJsonStr(failed));
                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) {
            // ignore
        } finally {
            if (writer != null) writer.close();
        }
    }
}

Testing the Idempotency Flow

A controller provides two endpoints: one to obtain a token and another annotated with @AutoIdempotent to test idempotent behavior. Using Postman, the first request succeeds, while subsequent requests return a duplicate‑operation error.

@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 result = new ResultVo();
            result.setCode(Constant.code_success);
            result.setMessage(Constant.SUCCESS);
            result.setData(token);
            return JSONUtil.toJsonStr(result);
        }
        return StrUtil.EMPTY;
    }

    @AutoIdempotent
    @PostMapping("/test/Idempotence")
    public String testIdempotence() {
        String businessResult = testService.testIdempotence();
        if (StrUtil.isNotEmpty(businessResult)) {
            ResultVo success = ResultVo.getSuccessResult(businessResult);
            return JSONUtil.toJsonStr(success);
        }
        return StrUtil.EMPTY;
    }
}

Diagram

Redis idempotency flow diagram
Redis idempotency flow diagram

Postman Request Screenshots

Get token response
Get token response
First business request success
First business request success
Second request duplicate error
Second request duplicate error

Conclusion

This article demonstrates a clean Spring Boot approach to achieve automatic API idempotency using Redis, custom annotations, token validation, and interceptors. By preventing repeated data modifications, the solution avoids dirty data, 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 BootInterceptorIdempotencyannotation
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.