Protecting endpoints
The filter only acts on protected requests. Every other request passes through untouched, even when it carries an Idempotency-Key header. A request is protected in one of two ways: its handler method has @Idempotent, or its path and method match idempotency.path-patterns and idempotency.methods.
On a method
Section titled “On a method”@Idempotent@PostMappingResponseEntity<Order> create(@RequestBody NewOrder body) { // ...}@Idempotent works on any handler method found through RequestMappingHandlerMapping, whatever its HTTP method. The tests cover POST and PATCH.
On a controller class
Section titled “On a controller class”Put the annotation on the class to protect every handler method in it:
@Idempotent@RestController@RequestMapping("/tasks")class TasksController {
@PostMapping @ResponseStatus(HttpStatus.CREATED) String create() { // ... }}The filter looks for the annotation on the method first and falls back to the class.
Optional keys
Section titled “Optional keys”By default a protected request without the header gets 400. With required = false, a request without the header runs the handler normally and nothing is stored. A request with the header is handled as usual.
@Idempotent(required = false)@PostMapping("/optional")Note optional(@RequestBody NewNote body) { // ...}Use this while clients are migrating to sending keys.
TTL per endpoint
Section titled “TTL per endpoint”A key is kept for idempotency.ttl after the first request, 24 hours by default. After that the same key runs the handler again. Override the TTL on one endpoint with ttl:
@Idempotent(ttl = "10m")@PostMapping("/short-ttl")Note shortTtl(@RequestBody NewNote body) { // ...}The value is parsed with Spring Boot’s DurationStyle.detectAndParse, so both the simple form ("10m", "24h") and ISO-8601 ("PT1H") work. An empty string, the default, means idempotency.ttl.
By configuration
Section titled “By configuration”Endpoints you cannot annotate, for example in a library or in functional endpoints (RouterFunction), can be protected by path:
idempotency.path-patterns=/api/orders/**,/api/payouts/**idempotency.methods=POST,PATCHidempotency.required=trueidempotency.path-patternsuses SpringPathPatternsyntax. The patterns are matched against the request path without the servlet context path.idempotency.methodslists the HTTP methods the patterns apply to. The default isPOST, PATCH. A GET to a matching path is not protected.idempotency.requireddecides whether matched requests need a key. It applies only to path patterns.- Path-pattern requests use
idempotency.ttl. There is no per-pattern TTL.
When a handler method has @Idempotent, the annotation’s settings win over any matching path pattern.
Handlers that cannot be protected
Section titled “Handlers that cannot be protected”@Idempotent on a handler whose return type is Callable, CompletionStage (which includes CompletableFuture), DeferredResult, WebAsyncTask, ResponseBodyEmitter (which includes SseEmitter) or StreamingResponseBody fails. The filter throws IllegalStateException with the message @Idempotent does not support asynchronous handlers: .... The check runs when a request reaches that handler, not at startup. The response of an asynchronous handler is written after the filter returns, so it cannot be stored.
Avoid @Idempotent on large uploads or downloads. The filter reads the whole request body into memory before the handler runs, and holds the whole response body until it is stored.
Cost for unprotected requests
Section titled “Cost for unprotected requests”To know whether a request is protected, the filter looks up the handler through the application’s RequestMappingHandlerMapping beans. That lookup runs for every request that reaches the filter, protected or not. The annotation result is cached per handler method.
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.