Ensuring API Idempotency with Spring, Redis, and Custom Annotations
This article explains the concept of idempotency, identifies which HTTP methods are naturally idempotent, and demonstrates a complete Java Spring solution—including custom annotations, AOP handling, token generation, and Redis storage—to guarantee safe retry and duplicate‑request protection.
1. What is Idempotency?
In simple terms, idempotency means that executing the same interface request multiple times yields the same result as a single execution. Achieving this in a production system requires careful design, and every API should be idempotent.
2. Which requests are naturally idempotent?
Read‑only (query) requests are inherently idempotent, and delete requests are usually idempotent except in ABA scenarios.
Simple example
If a delete‑A request times out and is automatically retried, an intervening insert of A could cause the second delete to remove a newly added record—an ABA problem, though it is often negligible.
Update operations are also idempotent in most cases, except when the update changes state based on current values, e.g., update table set a = a + 1 where v = 1, which is non‑idempotent. Insert operations are generally non‑idempotent unless a unique index prevents duplicates.
3. Why is idempotency needed?
1. Timeout retry
When an RPC request fails due to network instability, the client may retry. If the original request already reached the server, the server must ensure the operation is idempotent.
2. Asynchronous callbacks
Asynchronous callbacks increase throughput but must be idempotent to avoid duplicate processing.
3. Message queues
Frameworks such as Kafka, RocketMQ, and RabbitMQ follow an “at‑least‑once” delivery model, so consumers must implement idempotent handling.
4. Key factors for implementing idempotency
Factor 1
An idempotent token (or global ID) uniquely identifies a client request. It is usually generated by the client, though a third‑party service can also provide it.
Factor 2
With a unique token, the server must ensure the token is used only once, commonly by leveraging a database unique index.
5. Implementing idempotency with annotations
1. Custom annotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/** Parameter name from which the token will be extracted */
String name() default "";
/** Field name inside the parameter that holds the token */
String field() default "";
/** Parameter type */
Class type();
}2. Unified request data object
@Data
public class RequestData<T> {
private Header header;
private T body;
}
@Data
public class Header {
private String token;
}
@Data
public class Order {
String orderNo;
}3. AOP handling
import com.springboot.micrometer.annotation.Idempotent;
import com.springboot.micrometer.entity.RequestData;
import com.springboot.micrometer.idempotent.RedisIdempotentStorage;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Map;
@Aspect
@Component
public class IdempotentAspect {
@Resource
private RedisIdempotentStorage redisIdempotentStorage;
@Pointcut("@annotation(com.springboot.micrometer.annotation.Idempotent)")
public void idempotent() {}
@Around("idempotent()")
public Object methodAround(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Idempotent idempotent = method.getAnnotation(Idempotent.class);
String field = idempotent.field();
String name = idempotent.name();
Class clazzType = idempotent.type();
String token = "";
Object object = clazzType.newInstance();
Map<String, Object> paramValue = AopUtils.getParamValue(joinPoint);
if (object instanceof RequestData) {
RequestData idempotentEntity = (RequestData) paramValue.get(name);
token = String.valueOf(AopUtils.getFieldValue(idempotentEntity.getHeader(), field));
}
if (redisIdempotentStorage.delete(token)) {
return joinPoint.proceed();
}
return "重复请求";
}
} import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.CodeSignature;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class AopUtils {
public static Object getFieldValue(Object obj, String name) throws Exception {
Field[] fields = obj.getClass().getDeclaredFields();
Object object = null;
for (Field field : fields) {
field.setAccessible(true);
if (field.getName().toUpperCase().equals(name.toUpperCase())) {
object = field.get(obj);
break;
}
}
return object;
}
public static Map<String, Object> getParamValue(ProceedingJoinPoint joinPoint) {
Object[] paramValues = joinPoint.getArgs();
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Map<String, Object> param = new HashMap<>(paramNames.length);
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return param;
}
}4. Token generation
import com.springboot.micrometer.idempotent.RedisIdempotentStorage;
import com.springboot.micrometer.util.IdGeneratorUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/idGenerator")
public class IdGeneratorController {
@Resource
private RedisIdempotentStorage redisIdempotentStorage;
@RequestMapping("/getIdGeneratorToken")
public String getIdGeneratorToken() {
String generateId = IdGeneratorUtil.generateId();
redisIdempotentStorage.save(generateId);
return generateId;
}
} public interface IdempotentStorage {
void save(String idempotentId);
boolean delete(String idempotentId);
} import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
@Component
public class RedisIdempotentStorage implements IdempotentStorage {
@Resource
private RedisTemplate<String, Serializable> redisTemplate;
@Override
public void save(String idempotentId) {
redisTemplate.opsForValue().set(idempotentId, idempotentId, 10, TimeUnit.MINUTES);
}
@Override
public boolean delete(String idempotentId) {
return redisTemplate.delete(idempotentId);
}
} import java.util.UUID;
public class IdGeneratorUtil {
public static String generateId() {
return UUID.randomUUID().toString();
}
}5. Request example
Before calling the business API, obtain a token, then include the token in the request.
import com.springboot.micrometer.annotation.Idempotent;
import com.springboot.micrometer.entity.Order;
import com.springboot.micrometer.entity.RequestData;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
public class OrderController {
@RequestMapping("/saveOrder")
@Idempotent(name = "requestData", type = RequestData.class, field = "token")
public String saveOrder(@RequestBody RequestData<Order> requestData) {
return "success";
}
}First request succeeds, second request fails as the token has been consumed.
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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
