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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Idempotency keys let clients safely retry requests without triggering duplicate side effects.
- 2A pending-marker written with putIfAbsent turns a cache into a concurrency guard against in-flight duplicates.
- 3Evicting the key on failure keeps failed attempts retryable rather than permanently cached.
Related explainers
typescript
import { useEffect, useRef, useCallback } from "react"; type DraftPayload = Record<string, unknown>;
A debounced auto-save hook in React
debouncing
request-cancellation
refs
Advanced
7 steps
java
public final class ListPartitioner { private ListPartitioner() { }
Splitting a list into fixed-size chunks in Java
generics
partitioning
streams
Intermediate
8 steps
go
package handlers import ( "net/http"
Content negotiation in Gin handlers
content-negotiation
struct-tags
rest-api
Intermediate
4 steps
java
package com.example.orders.messaging; import com.example.orders.dto.OrderEvent; import com.example.orders.service.OrderProcessingService;
Manual RabbitMQ acks in a Spring listener
message-queue
error-handling
acknowledgement
Intermediate
6 steps
java
package com.example.shop.repository; import com.example.shop.domain.Order; import com.example.shop.domain.OrderStatus;
Deriving JPA queries from method names in Spring
query derivation
repositories
pagination
Intermediate
8 steps
typescript
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpHandler,
Refreshing tokens with an Angular HttpInterceptor
http-interceptor
token-refresh
rxjs
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-an-idempotency-aspect-works-in-spring-explained-java-72ec/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.