java 52 lines · 9 steps

How an idempotency aspect works in Spring

A Spring AOP aspect caches responses by Idempotency-Key so retried requests replay the same result instead of running twice.

Explained by highlit
1@Aspect
2@Component
3public class IdempotencyAspect {
4 
5 private static final String CACHE_NAME = "idempotencyKeys";
6 
7 private final CacheManager cacheManager;
8 private final HttpServletRequest request;
9 private final ObjectMapper objectMapper;
10 
11 public IdempotencyAspect(CacheManager cacheManager,
12 HttpServletRequest request,
13 ObjectMapper objectMapper) {
14 this.cacheManager = cacheManager;
15 this.request = request;
16 this.objectMapper = objectMapper;
17 }
18 
19 @Around("@annotation(idempotent)")
20 public Object enforce(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable {
21 String key = request.getHeader("Idempotency-Key");
22 if (key == null || key.isBlank()) {
23 throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing Idempotency-Key header");
24 }
25 
26 Cache cache = cacheManager.getCache(CACHE_NAME);
27 String cacheKey = idempotent.scope() + ":" + key;
28 
29 Cache.ValueWrapper cached = cache.get(cacheKey);
30 if (cached != null) {
31 CachedResponse stored = objectMapper.convertValue(cached.get(), CachedResponse.class);
32 return ResponseEntity
33 .status(stored.status())
34 .header("Idempotent-Replayed", "true")
35 .body(stored.body());
36 }
37 
38 if (cache.putIfAbsent(cacheKey, CachedResponse.pending()) != null) {
39 throw new ResponseStatusException(HttpStatus.CONFLICT, "Request with this key is already in progress");
40 }
41 
42 try {
43 Object result = pjp.proceed();
44 ResponseEntity<?> response = (ResponseEntity<?>) result;
45 cache.put(cacheKey, new CachedResponse(response.getStatusCode().value(), response.getBody()));
46 return result;
47 } catch (Throwable ex) {
48 cache.evict(cacheKey);
49 throw ex;
50 }
51 }
52}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Idempotency keys let clients safely retry requests without triggering duplicate side effects.
  2. 2A pending-marker written with putIfAbsent turns a cache into a concurrency guard against in-flight duplicates.
  3. 3Evicting the key on failure keeps failed attempts retryable rather than permanently cached.

Related explainers

Share this explainer

Here's the card — post it anywhere.

How an idempotency aspect works in Spring — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code