Implementing API Idempotency in Spring Boot Using Redis and Token Mechanism

This article explains the concept of API idempotency, presents common solutions such as unique indexes, token mechanisms, and distributed locks, and provides a complete Spring Boot implementation using Redis to generate, store, and validate tokens via custom annotations, interceptors, and service classes, including testing and performance verification.

Top Architect
Top Architect
Top Architect
Implementing API Idempotency in Spring Boot Using Redis and Token Mechanism

Idempotency means that multiple identical requests to an interface must result in a single operation execution, which is essential for order creation, payment processing, callback handling, and form submissions.

Common solutions include unique indexes, token mechanisms, pessimistic/optimistic locks, distributed locks (Redis or Zookeeper), and state machines.

Implementation Overview

The article adopts the token‑based approach: each request generates a unique token stored in Redis; the token is sent in the request header or parameter, and the backend checks Redis for its existence before processing.

Project Dependencies (pom.xml)

<!-- Redis-Jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

<!-- Lombok (for @Slf4j) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.10</version>
</dependency>

JedisUtil – Redis helper class

package com.wangzaiplus.test.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

@Component
@Slf4j
public class JedisUtil {
    @Autowired
    private JedisPool jedisPool;
    private Jedis getJedis() { return jedisPool.getResource(); }
    public String set(String key, String value) { /* ... */ }
    public String set(String key, String value, int expireTime) { /* ... */ }
    public String get(String key) { /* ... */ }
    public Long del(String key) { /* ... */ }
    public Boolean exists(String key) { /* ... */ }
    public Long expire(String key, int expireTime) { /* ... */ }
    public Long ttl(String key) { /* ... */ }
    private void close(Jedis jedis) { if (jedis != null) jedis.close(); }
}

Custom Annotation @ApiIdempotent

package com.wangzaiplus.test.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** Apply on controller methods that require idempotency */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent { }

ApiIdempotentInterceptor – Interceptor that checks the token

package com.wangzaiplus.test.interceptor;

import com.wangzaiplus.test.annotation.ApiIdempotent;
import com.wangzaiplus.test.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

public class ApiIdempotentInterceptor implements HandlerInterceptor {
    @Autowired
    private TokenService tokenService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) return true;
        Method method = ((HandlerMethod) handler).getMethod();
        ApiIdempotent anno = method.getAnnotation(ApiIdempotent.class);
        if (anno != null) {
            tokenService.checkToken(request);
        }
        return true;
    }
    // postHandle and afterCompletion omitted
}

TokenServiceImpl – Generates and validates tokens

package com.wangzaiplus.test.service.impl;

import com.wangzaiplus.test.common.Constant;
import com.wangzaiplus.test.common.ResponseCode;
import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.exception.ServiceException;
import com.wangzaiplus.test.service.TokenService;
import com.wangzaiplus.test.util.JedisUtil;
import com.wangzaiplus.test.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;

@Service
public class TokenServiceImpl implements TokenService {
    private static final String TOKEN_NAME = "token";
    @Autowired
    private JedisUtil jedisUtil;
    @Override
    public ServerResponse createToken() {
        String uuid = RandomUtil.UUID32();
        String token = Constant.Redis.TOKEN_PREFIX + uuid;
        jedisUtil.set(token, token, Constant.Redis.EXPIRE_TIME_MINUTE);
        return ServerResponse.success(token);
    }
    @Override
    public void checkToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_NAME);
        if (StringUtils.isBlank(token)) token = request.getParameter(TOKEN_NAME);
        if (StringUtils.isBlank(token)) throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg());
        if (!jedisUtil.exists(token)) throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
        Long del = jedisUtil.del(token);
        if (del == null || del <= 0) throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
    }
}

Spring Boot Application Configuration

package com.wangzaiplus.test;

import com.wangzaiplus.test.interceptor.ApiIdempotentInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@SpringBootApplication
@MapperScan("com.wangzaiplus.test.mapper")
public class TestApplication extends WebMvcConfigurerAdapter {
    public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); }
    @Bean
    public CorsFilter corsFilter() { /* CORS config omitted */ }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiIdempotentInterceptor());
        super.addInterceptors(registry);
    }
    @Bean
    public ApiIdempotentInterceptor apiIdempotentInterceptor() { return new ApiIdempotentInterceptor(); }
}

Controller for Token Generation

package com.wangzaiplus.test.controller;

import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/token")
public class TokenController {
    @Autowired
    private TokenService tokenService;
    @GetMapping
    public ServerResponse token() { return tokenService.createToken(); }
}

Test Controller Using @ApiIdempotent

package com.wangzaiplus.test.controller;

import com.wangzaiplus.test.annotation.ApiIdempotent;
import com.wangzaiplus.test.common.ServerResponse;
import com.wangzaiplus.test.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {
    @Autowired
    private TestService testService;
    @ApiIdempotent
    @PostMapping("testIdempotence")
    public ServerResponse testIdempotence() { return testService.testIdempotence(); }
}

The article then describes how to obtain a token via the /token endpoint, store it in Redis, and use JMeter to simulate 50 concurrent requests with the same token to verify that only one request succeeds while duplicates are rejected. It also highlights the importance of checking the delete result to avoid race conditions.

Finally, the author summarizes that ensuring a unique token per request guarantees idempotency, and the interceptor‑annotation pattern simplifies the implementation, though AOP could be used as an alternative.

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 BootAPIInterceptorIdempotencyToken
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.