Implementing Automatic Idempotency in Spring Boot with Redis and Interceptors

This article explains how to achieve automatic request idempotency in Spring Boot applications by using Redis for token storage, custom @AutoIdempotent annotation, and a web interceptor that validates tokens, providing code examples, configuration steps, and testing procedures.

Architect
Architect
Architect
Implementing Automatic Idempotency in Spring Boot with Redis and Interceptors

In real development projects, an exposed API may receive many requests, so ensuring idempotency—where multiple executions have the same effect as a single execution—is essential to prevent duplicate database operations.

Common ways to guarantee idempotency include:

Creating a unique index in the database.

Using a token mechanism: generate a token before each request and validate it in the request header; after successful validation the token is deleted.

Applying pessimistic or optimistic locks.

Query‑then‑check: query the database first; if the record exists, reject the request, otherwise allow it.

The article then shows how to build a Redis‑based idempotency service.

1. Set up Redis service API

First, start a Redis server and add the Spring Boot Redis starter (or Jedis). The main API used is set and exists via RedisTemplate.

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

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

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

    /** Check if key exists */
    public boolean exists(final String key) { return redisTemplate.hasKey(key); }

    /** Read cache */
    public Object get(final String key) {
        ValueOperations<Serializable, Object> ops = redisTemplate.opsForValue();
        return ops.get(key);
    }

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

2. Define a custom annotation

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

3. Token service

Interface:

public interface TokenService {
    /** Create token */
    String createToken();
    /** Verify token */
    boolean checkToken(HttpServletRequest request) throws Exception;
}

Implementation uses RedisService to store a UUID token with a 10,000‑second TTL, validates the token from request header or parameter, and removes it after successful verification.

@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 e) { e.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);
        if (!redisService.remove(token)) throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
        return true;
    }
}

4. Interceptor configuration

Web configuration adds AutoIdempotentInterceptor to the 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);
    }
}

5. Interceptor implementation

@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 anno = method.getAnnotation(AutoIdempotent.class);
        if (anno != 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) { }
        finally { if (writer != null) writer.close(); }
    }
}

6. Test controller

A controller provides two endpoints: /get/token to obtain a token and /test/Idempotence (annotated with @AutoIdempotent) to simulate a business operation. Using Postman, the first request succeeds, while subsequent requests with the same token return a “repetitive operation” error, demonstrating the idempotency enforcement.

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

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

The article concludes that using Spring Boot, Redis, custom annotations, and interceptors provides an elegant, automated solution for API idempotency, preventing duplicate data modifications, reducing unnecessary concurrency, and improving 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.

JavaSpring BootInterceptor
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.