Achieve Perfect API Idempotency with Spring Boot, Redis, and Custom Annotations

Learn how to ensure API idempotency in Spring Boot applications by leveraging Redis for token management, creating a custom @AutoIdempotent annotation, configuring interceptors, and handling token creation and validation to prevent duplicate submissions and maintain data integrity.

ITFLY8 Architecture Home
ITFLY8 Architecture Home
ITFLY8 Architecture Home
Achieve Perfect API Idempotency with Spring Boot, Redis, and Custom Annotations

Introduction

In real development projects, an exposed API often faces a sudden flood of duplicate requests. To filter out these duplicates and protect business logic, implementing idempotency is essential.

What Is Idempotency?

Idempotency means that executing an operation any number of times has the same effect as executing it once. In practice, this means the impact on the database must be applied only once.

Common ways to guarantee idempotency include:

Creating a unique index in the database to ensure only one row is inserted.

Using a token mechanism: obtain a token before each request, attach it to the request header, and let the backend validate and delete it after use.

Applying pessimistic or optimistic locks to prevent concurrent updates.

Query‑then‑decide: check if a record already exists before processing.

Below is a diagram of how Redis can be used to achieve automatic idempotency.

Redis idempotency flowchart
Redis idempotency flowchart

1. Set Up Redis Service API

1. Deploy a Redis server.

2. Add the Spring Boot Redis starter (or Jedis) and use RedisTemplate for set and exists operations.

@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 existence */
    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. Custom Annotation @AutoIdempotent

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

3. Token Creation and Validation

Define a service interface with methods for creating and checking tokens.

public interface TokenService {
    /** Create token */
    String createToken();

    /** Validate token */
    boolean checkToken(HttpServletRequest request) throws Exception;
}

Implementation uses Redis to store a UUID token with an expiration time.

@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

Add the interceptor to Spring MVC configuration so that methods annotated with @AutoIdempotent are checked.

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

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry);
    }
}

Interceptor logic:

@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 {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try (PrintWriter writer = response.getWriter()) {
            writer.print(json);
        } catch (IOException e) {
            // ignore
        }
    }
}

5. Test Cases

Controller to obtain a token and test idempotent endpoint.

@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;
    }
}

Using Postman, first call /get/token to retrieve a token, then include the token in the header of the /test/Idempotence request. The first call succeeds; subsequent calls return a duplicate‑operation error, demonstrating effective idempotency.

Get token response
Get token response
First successful request
First successful request
Duplicate request error
Duplicate request error

6. Conclusion

This article demonstrates how to elegantly achieve API idempotency using Spring Boot, Redis, a custom annotation, and an interceptor. Idempotency is crucial for preventing dirty or inconsistent data when an endpoint is called repeatedly. The automated approach shown improves scalability and reduces the need for manual duplicate‑check logic.

Source: https://www.jianshu.com/p/9aef0c40b1b9

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.

redisSpring BootInterceptorannotationsIdempotency
ITFLY8 Architecture Home
Written by

ITFLY8 Architecture Home

ITFLY8 Architecture Home - focused on architecture knowledge sharing and exchange, covering project management and product design. Includes large-scale distributed website architecture (high performance, high availability, caching, message queues...), design patterns, architecture patterns, big data, project management (SCRUM, PMP, Prince2), product design, and more.

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.