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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
