Understanding Idempotency and Its Implementation with Custom Annotations in Java
This article explains the concept of idempotency, identifies which HTTP requests are naturally idempotent, discusses why idempotency is essential for retries, async callbacks, and message queues, and demonstrates a practical Java implementation using custom annotations, AOP, and Redis for token management.
Idempotency means that executing the same interface multiple times yields the same result as a single execution; achieving this in production requires careful design.
Read‑only requests are naturally idempotent, and delete operations are usually idempotent except in ABA scenarios. Updates are generally idempotent unless they involve non‑deterministic statements such as update table set a = a + 1 where v = 1. Insert operations are non‑idempotent unless a unique database index prevents duplicates.
Idempotency is needed for timeout retries (so a request that may have already been processed is not executed twice), asynchronous callbacks (which must be safe against duplicate deliveries), and message‑queue consumption (most queues use an "at‑least‑once" delivery model).
The key factors for implementing idempotency are:
A unique idempotency token generated by the client (or a third‑party) for each request.
Server‑side enforcement that the token is used only once, often by leveraging a unique index in a database or a distributed cache.
The following Java example shows how to create a custom annotation @Idempotent, a request wrapper, and an AOP aspect that checks the token against Redis before proceeding.
import java.lang.annotation.ElementType;<br/>import java.lang.annotation.Retention;<br/>import java.lang.annotation.RetentionPolicy;<br/>import java.lang.annotation.Target;<br/><br/>@Target(value = ElementType.METHOD)<br/>@Retention(RetentionPolicy.RUNTIME)<br/>public @interface Idempotent {<br/> String name() default "";<br/> String field() default "";<br/> Class type();<br/>} @Data<br/>public class RequestData<T> {<br/> private Header header;<br/> private T body;<br/>}<br/><br/>@Data<br/>public class Header {<br/> private String token;<br/>}<br/><br/>@Data<br/>public class Order {<br/> String orderNo;<br/>} @Aspect<br/>@Component<br/>public class IdempotentAspect {<br/> @Resource<br/> private RedisIdempotentStorage redisIdempotentStorage;<br/><br/> @Pointcut("@annotation(com.springboot.micrometer.annotation.Idempotent)")<br/> public void idempotent() {}<br/><br/> @Around("idempotent()")<br/> public Object methodAround(ProceedingJoinPoint joinPoint) throws Throwable {<br/> MethodSignature signature = (MethodSignature) joinPoint.getSignature();<br/> Method method = signature.getMethod();<br/> Idempotent idempotent = method.getAnnotation(Idempotent.class);<br/> String field = idempotent.field();<br/> String name = idempotent.name();<br/> Class clazzType = idempotent.type();<br/> String token = "";<br/> Object object = clazzType.newInstance();<br/> Map<String, Object> paramValue = AopUtils.getParamValue(joinPoint);<br/> if (object instanceof RequestData) {<br/> RequestData idempotentEntity = (RequestData) paramValue.get(name);<br/> token = String.valueOf(AopUtils.getFieldValue(idempotentEntity.getHeader(), field));<br/> }<br/> if (redisIdempotentStorage.delete(token)) {<br/> return joinPoint.proceed();<br/> }<br/> return "重复请求";<br/> }<br/>} public class RedisIdempotentStorage implements IdempotentStorage {<br/> @Resource<br/> private RedisTemplate<String, Serializable> redisTemplate;<br/><br/> @Override<br/> public void save(String idempotentId) {<br/> redisTemplate.opsForValue().set(idempotentId, idempotentId, 10, TimeUnit.MINUTES);<br/> }<br/><br/> @Override<br/> public boolean delete(String idempotentId) {<br/> return redisTemplate.delete(idempotentId);<br/> }<br/>} @RestController<br/>@RequestMapping("/order")<br/>public class OrderController {<br/> @RequestMapping("/saveOrder")<br/> @Idempotent(name = "requestData", type = RequestData.class, field = "token")<br/> public String saveOrder(@RequestBody RequestData<Order> requestData) {<br/> return "success";<br/> }<br/>}Clients first obtain a token via a dedicated endpoint, then include that token in the request header; the first request succeeds, while subsequent duplicate submissions are rejected.
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.
