Skip to content

Open source projects by Jean Carlo Benites Porras.aquilabot.dev

Quick start

This page assumes a Spring Boot 4.1 application with spring-boot-starter-webmvc and the library on the classpath. See installation.

OrdersController.java
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.

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.

Terminal window
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 201
Location: /orders/1
Content-Type: application/json
{"id":1,"item":"notebook","quantity":2}

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 201
Content-Type: application/json
Location: /orders/1
Idempotent-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.

Change the body and keep the key:

Terminal window
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 422
Content-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"}

@Idempotent requires the header by default:

Terminal window
curl -si -X POST localhost:8080/orders \
-H 'Content-Type: application/json' \
-d '{"item":"notebook","quantity":2}'
HTTP/1.1 400
Content-Type: application/problem+json
{"type":"about:blank","title":"Bad Request","status":400,"detail":"This request requires an Idempotency-Key header.","instance":"/orders"}

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.