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.
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.
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.
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.
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.
