Quick start
This page assumes a Spring Boot 4.1 application with spring-boot-starter-webmvc and the library on the classpath. See installation.
Annotate a controller method
Section titled “Annotate a controller method”import dev.aquilabot.idempotency.Idempotent;import java.net.URI;import java.util.concurrent.atomic.AtomicLong;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;
@RestController@RequestMapping("/orders")class OrdersController {
record NewOrder(String item, int quantity) { }
record Order(long id, String item, int quantity) { }
private final AtomicLong ids = new AtomicLong();
@Idempotent @PostMapping ResponseEntity<Order> create(@RequestBody NewOrder body) { Order order = new Order(ids.incrementAndGet(), body.item(), body.quantity()); return ResponseEntity.created(URI.create("/orders/" + order.id())).body(order); }}No configuration is needed. The default store keeps keys in memory.
Send a request
Section titled “Send a request”Start the application and send a POST with an Idempotency-Key header. The draft defines the key as a quoted string, so the value includes the double quotes.
curl -si -X POST localhost:8080/orders \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: "order-4711"' \ -d '{"item":"notebook","quantity":2}'The handler runs and the response is stored. Output, trimmed to the relevant headers:
HTTP/1.1 201Location: /orders/1Content-Type: application/json
{"id":1,"item":"notebook","quantity":2}Retry with the same key
Section titled “Retry with the same key”Run the same command again. The handler does not run. The filter replays the stored status, headers and body, and adds Idempotent-Replayed: true:
HTTP/1.1 201Content-Type: application/jsonLocation: /orders/1Idempotent-Replayed: true
{"id":1,"item":"notebook","quantity":2}The order id is still 1, because create ran once. A bare value such as -H 'Idempotency-Key: order-4711' is the same key as "order-4711" and is replayed too.
Reuse the key for a different request
Section titled “Reuse the key for a different request”Change the body and keep the key:
curl -si -X POST localhost:8080/orders \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: "order-4711"' \ -d '{"item":"notebook","quantity":3}'The fingerprint no longer matches, so the filter answers 422 without running the handler:
HTTP/1.1 422Content-Type: application/problem+json
{"type":"about:blank","title":"Unprocessable Content","status":422,"detail":"This Idempotency-Key was already used for a different request.","instance":"/orders"}Leave the key out
Section titled “Leave the key out”@Idempotent requires the header by default:
curl -si -X POST localhost:8080/orders \ -H 'Content-Type: application/json' \ -d '{"item":"notebook","quantity":2}'HTTP/1.1 400Content-Type: application/problem+json
{"type":"about:blank","title":"Bad Request","status":400,"detail":"This request requires an Idempotency-Key header.","instance":"/orders"}Next steps
Section titled “Next steps”- Make the key optional, set a TTL per endpoint, or protect paths without annotations: protecting endpoints.
- Run more than one instance: choosing a store.
- All status codes and bodies: error responses.
Written from the code and tests of each repository. If a page and the code disagree, the code is right and the page has a bug. Open an issue.
© 2026 Jean Carlo Benites Porras. Code samples are MIT, like the projects they come from.