Implementing Automatic Idempotency in Spring Boot Using Redis and Custom Annotations

This article explains how to implement automatic API idempotency in Spring Boot by using Redis for token storage, a custom @AutoIdempotent annotation, and a request interceptor, providing code examples and test steps to ensure duplicate requests are rejected.

Architecture Digest
Architecture Digest
Architecture Digest
Implementing Automatic Idempotency in Spring Boot Using Redis and Custom Annotations

Introduction

In real projects, exposed APIs may receive a burst of duplicate requests; implementing idempotency prevents repeated processing and protects business logic.

Idempotency Concept

Idempotency means that executing an operation multiple times has the same effect as executing it once; the database impact should occur only once.

Create a unique index in the database.

Use a token mechanism.

Apply pessimistic or optimistic locks.

Check‑then‑act pattern.

Redis Service API

A Spring Boot RedisTemplate (or Jedis) wrapper provides basic cache operations.

/**
 * redis工具类
 */
@Component
public class RedisService {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 写入缓存
     * @param key
     * @param value
     * @return
     */
    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;
    }

    /**
     * 写入缓存并设置过期时间
     * @param key
     * @param value
     * @param expireTime
     * @return
     */
    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;
    }

    /**
     * 判断缓存中是否有对应的value
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 读取缓存
     * @param key
     * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }

    /**
     * 删除对应的value
     * @param key
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;
    }
}

Custom Annotation @AutoIdempotent

The annotation can be placed on methods and is retained at runtime.

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

Token Creation and Verification

A TokenService interface defines createToken and checkToken methods; the implementation stores tokens in Redis with an expiration time and validates them from request headers or parameters.

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

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

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

Interceptor Configuration

The interceptor is added to Spring MVC configuration; it checks for the @AutoIdempotent annotation, invokes tokenService.checkToken, and returns a JSON error on failure.

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

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry);
    }
}
/**
 * 拦截器
 */
@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;
        }
        HandlerMethod hm = (HandlerMethod) handler;
        Method method = hm.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;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}

    private void writeReturnJson(HttpServletResponse response, String json) throws Exception {
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try {
            writer = response.getWriter();
            writer.print(json);
        } catch (IOException e) {
        } finally {
            if (writer != null) writer.close();
        }
    }
}

Test Case

A controller provides /get/token to obtain a token and /test/Idempotence annotated with @AutoIdempotent. Using Postman, the first request succeeds and subsequent duplicate requests are rejected.

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

Conclusion

The article demonstrates an elegant way to achieve API idempotency with Spring Boot, Redis, custom annotations, and interceptors, improving data consistency, preventing dirty data, and reducing unnecessary load.

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.

JavaredisInterceptorSpringBootannotation
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

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.