⏱️ Lectura: 7 min
Picture paying online, the page hangs, you click again… and the familiar fear kicks in: “did they just charge me twice?”. That problem —operations running twice when the client retries— is one of the most expensive in payment systems and financial APIs. The open source extension quarkus-http-idempotency, published on Quarkiverse and available on Maven Central, solves it at the server level without you writing any special logic: it makes a POST or PATCH happen exactly once, even when it arrives more than once.
📑 En este artículo
- The problem: retries are not free
- The solution: the Idempotency-Key header
- What quarkus-http-idempotency is
- Installation
- Quick start
- How it behaves in each case
- Safe for multi-user and multi-tenant
- Stores: local memory or distributed Redis
- Configuration
- Best practices
- Security and robustness
- Frequently Asked Questions
- References
The problem: retries are not free
On the web, retries are unavoidable: a dropped connection, a timeout, an impatient user double-clicking, an HTTP client configured to retry automatically. When the request is read-only (GET), retrying is harmless. But when the request changes something —creating an order, charging a card, transferring money— running it twice can mean two charges, two orders, or two transfers.
The hard part is that the client often doesn’t know whether the first request made it through. If the server processed the charge but the response was lost on the way back, the client thinks it failed and retries. The damage is already done.
The solution: the Idempotency-Key header
The industry solved this with a simple, elegant pattern, popularized by Stripe and formalized in an IETF draft: the Idempotency-Key header.
The idea: the client generates a unique key (for example, a UUID) and sends it with the request. The server remembers that key. If a second request arrives with the same key, the server does not run the operation again: it simply returns the stored response from the first time. The side effect happens exactly once; the client always gets the same answer.
We’ve already covered the concept and its edge cases in depth in previous articles. This one focuses on the tool that implements it in Quarkus in a production-ready way.
What quarkus-http-idempotency is
It’s a Quarkus extension that implements the Idempotency-Key pattern transparently. Its biggest strength: once it’s on the classpath, no code changes are required. Any POST or PATCH carrying an Idempotency-Key header is handled idempotently, automatically.
Key features:
- Spec-aligned. Follows the IETF draft:
409/422/400error semantics, a request-body fingerprint, a documented expiry policy, and RFC 9457 (application/problem+json) error bodies. - Safe for multi-user and multi-tenant APIs. The store key is a per-caller composite:
SHA-256(principal ⊕ scope ⊕ raw-key). That way, one caller can never be served another caller’s stored response. Optional per-tenant isolation via a trusted scope header. - Pluggable stores. Bounded in-memory (Caffeine) for a single node, or distributed Redis for clusters, behind a small SPI.
- Hardened. Bounded memory, capped fingerprint/stored-body sizes, and a deny-list that keeps credential headers (
Set-Cookie,Authorization,*-token…) out of stored responses. - Reactive-ready. Works with
Uni/async return types.
Installation
Add the dependency to your pom.xml:
<dependency>
<groupId>io.quarkiverse.idempotency</groupId>
<artifactId>quarkus-http-idempotency</artifactId>
<version>${quarkus-http-idempotency.version}</version>
</dependency>
With the extension on the classpath, any POST/PATCH carrying an Idempotency-Key header is handled idempotently. You don’t need to touch your endpoints.
Quick start
Let’s see the behavior with two identical calls:
# First call — runs the operation
curl -i -H "Idempotency-Key: 8e039f93" -H "Content-Type: application/json" \
-d '{"item":"widget"}' https://api.example.com/orders
# HTTP/1.1 201 Created · Location: /orders/order-1
# Retry with the SAME key — replays the stored response,
# the order is NOT created again
curl -i -H "Idempotency-Key: 8e039f93" -H "Content-Type: application/json" \
-d '{"item":"widget"}' https://api.example.com/orders
# HTTP/1.1 201 Created · Idempotent-Replayed: true
The second response carries the Idempotent-Replayed: true header, signaling it was a replay and not a fresh execution.
How it behaves in each case
The extension covers the edge cases that naive logic tends to ignore:
| Situation | Behavior | HTTP status |
|---|---|---|
| New key | Reserve, run the handler, store and return the response | the handler’s |
| Same key, same payload, completed | Replay the stored status, body and headers | the stored one |
| Same key, same payload, still in flight | Reject — a concurrent retry is in progress | 409 |
| Same key, different payload | Reject — key reused for a different request | 422 |
| Key required but missing/invalid | Reject | 400 |
The 422 case matters: if a client reuses an idempotency key for a different payload, it’s almost always a client bug. Rejecting it prevents silently corrupting data.
Safe for multi-user and multi-tenant
A common mistake when rolling this by hand is storing responses in a global cache keyed only by the raw key. The problem: if two different users pick the same key (or an attacker guesses one), one could receive the other’s response —a serious data leak.
quarkus-http-idempotency prevents this by making the store key a per-caller composite: SHA-256(principal ⊕ scope ⊕ raw-key). The principal is the authenticated identity; the scope allows extra isolation. The result: one user’s key lives in a space separate from anyone else’s, and they never cross.
For multi-tenant scenarios, you can enable per-tenant isolation via a trusted scope header, so each tenant gets its own idempotency space.
Stores: local memory or distributed Redis
The extension separates the idempotency logic from where responses are stored, behind an SPI:
in-memory(default) — a Caffeine cache bounded bymax-entries. Ideal for a single node or development.redis— for multi-node clusters. Add the Redis client and switch the store:
# application.properties
quarkus.idempotency.store=redis
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-redis-client</artifactId>
</dependency>
The Redis store reserves each key with a single atomic SET NX GET PX round-trip (requires Redis 7.0+). This guarantees that even with many nodes handling retries in parallel, only one wins the reservation and runs the operation.
Configuration
All properties live under the quarkus.idempotency.* prefix: header name, guarded methods, TTLs, store backend, per-tenant scope, and resource bounds. A typical example:
# Enable the distributed store
quarkus.idempotency.store=redis
# (Illustrative; check the official docs for the exact property
# names and default values)
# - idempotency header name
# - guarded HTTP methods (POST, PATCH)
# - time-to-live of stored keys (TTL)
# - size limits for fingerprint and stored body
The full property reference and the security model are in the official documentation.
Best practices
- Generate the key on the client, one per operation intent. A fresh UUID v4 per user action (not per retry). All retries of that action share the same key.
- Guard only state-changing methods.
POSTandPATCHneed it;GET,HEAD, and a naturally idempotentPUTdon’t. - Set a TTL matching your retry window. Keys shouldn’t live forever: long enough to cover realistic retries (minutes to hours, depending on the case).
- Use Redis in production with more than one node. The in-memory cache isn’t shared across instances; in a cluster, two nodes could run the same operation. Redis with the atomic reservation prevents that.
- Never store credentials in the response. The extension already excludes sensitive headers by default; don’t turn that off.
- Don’t reuse a key for different payloads. If the operation changes, change the key. The
422is there to catch you if you slip.
Security and robustness
The extension is hardened for production:
- Bounded memory. The in-memory store has an entry cap; it won’t grow unbounded under a flood of keys.
- Capped sizes. The body fingerprint and the stored response have limits, preventing a giant body from exhausting memory.
- Unconditional deny-list. Headers like
Set-Cookie,Authorization, and anything ending in-tokenare never stored in the replayed response, avoiding credential leakage across retries. - Standard errors. Rejections use RFC 9457 (
application/problem+json) with aLinkto the docs, easy for clients to consume.
Frequently Asked Questions
Do I have to change my code to use it? No. With the extension on the classpath, any POST/PATCH with the Idempotency-Key header is handled automatically.
What happens if the client doesn’t send the header? It depends on your configuration: the request can be processed normally (with no idempotency guarantee) or rejected with 400 if you require the key.
Is it useful for microservices? Yes, especially. In distributed architectures with automatic retries between services, idempotency is essential. Use the Redis store to share state across instances.
Does it work with reactive code? Yes, it supports Uni/async return types.
0 Comments