Idempotency
How to design operations that can run multiple times without duplicate side effects, essential for safe retries and reliable event processing.
What problem it solves
In distributed systems, messages and requests can arrive more than once for a variety of reasons:
- The client retries a request because it didnβt get a response (but the server did process it)
- The messaging broker delivers a duplicate event (at-least-once guarantee)
- A network failure causes the acknowledgment to be lost, and the sender resends
- The user double-clicks the βPayβ button
Without idempotency:
Client βββΊ POST /payments {amount: 100}
β
Payment processed (but response is lost)
Client βββΊ POST /payments {amount: 100} (retry)
β
Payment processed AGAIN
Result: Charged $200 instead of $100
Without idempotency, retries and duplicates cause repeated side effects: double charges, inventory deducted twice, duplicate emails, duplicate records in the database.
How it works
An operation is idempotent when running it once produces the same result as running it multiple times. The system detects duplicate requests and returns the original result without executing the operation again.
With idempotency:
Client βββΊ POST /payments
Idempotency-Key: "PAY-abc-123"
{amount: 100}
β
Payment processed β response is lost
Client βββΊ POST /payments
Idempotency-Key: "PAY-abc-123" (same key)
{amount: 100}
β
Returns previous result (without processing again)
Result: Charged $100 only once
Mechanism with an Idempotency Key
The most common approach is to use an idempotency key that uniquely identifies each operation:
1. Client generates a unique ID: "PAY-abc-123"
2. Client sends request with Idempotency-Key header
3. Server checks whether it already processed that key:
a. If it does NOT exist β process the operation, store result with the key
b. If it DOES exist β return the stored result without reprocessing
Storing keys
| Storage | Advantages | Disadvantages |
|---|---|---|
| Database | Persistent, transactional | Slower, requires cleanup |
| Redis/Cache | Fast, automatic TTL | May lose data on restart |
| Dedicated table | Clear separation, easy to query | Additional table to maintain |
Full flow example
Table: idempotency_keys
ββββββββββββββββββββ¬βββββββββ¬βββββββββββββββββββ¬ββββββββββ
β key β status β response β expires β
ββββββββββββββββββββΌβββββββββΌβββββββββββββββββββΌββββββββββ€
β PAY-abc-123 β done β {paymentId: 456} β 24h β
β PAY-def-456 β doing β null β 24h β
ββββββββββββββββββββ΄βββββββββ΄βββββββββββββββββββ΄ββββββββββ
Request 1: POST /payments (key: PAY-abc-123)
β Key does not exist β INSERT key with status "doing"
β Process payment β UPDATE key with status "done" and response
β Return response
Request 2: POST /payments (key: PAY-abc-123) [retry]
β Key exists with status "done"
β Return stored response (without reprocessing)
Request 3: POST /payments (key: PAY-def-456)
β Key exists with status "doing" (in progress)
β Return HTTP 409 Conflict (wait and retry)
Idempotency in event consumers
For event consumers, idempotency is implemented by tracking which events have already been processed:
Event received: OrderCreated (eventId: "EVT-789")
1. Check whether eventId was already processed
β SELECT * FROM processed_events WHERE event_id = 'EVT-789'
2a. If it does NOT exist:
β Process event (reserve inventory)
β INSERT INTO processed_events (event_id, processed_at)
β ACK to the broker
2b. If it DOES exist:
β Ignore (already processed)
β ACK to the broker
Naturally idempotent operations
Some operations are idempotent by nature and donβt need additional mechanisms:
β
Naturally idempotent:
PUT /users/123 {name: "MarΓa"} β Always leaves the same state
DELETE /orders/456 β Deleting something already deleted = no-op
GET /products/789 β Read, no side effects
β NOT idempotent (need a mechanism):
POST /payments {amount: 100} β Each execution creates a new payment
POST /orders {items: [...]} β Each execution creates a new order
POST /notifications {msg: "Hola"} β Each execution sends a message
Advantages
- Safe retries: Clients can retry without fear of duplicate effects
- Reliable event processing: Consumers handle duplicates correctly
- User experience: Double-clicking causes no problems
- Data consistency: No duplicate records or double charges
- Simplifies resilience: The Retry pattern works correctly when the downstream is idempotent
- Auditing: The record of idempotency keys serves as an operation log
Trade-offs / Disadvantages
- Additional storage: You need to store the keys and their results
- Implementation complexity: Handling concurrent states (doing/done) requires care
- Key cleanup: Old keys must be cleaned up periodically (TTL)
- Key generation: The client must generate unique, consistent keys
- Latency: Duplicate checking adds an extra query per operation
- Transactional consistency: The key and the operation must be saved in the same transaction
When to use
- Payment operations and financial transactions
- Any endpoint that receives retries (POST, PATCH with side effects)
- Event consumers in event-driven architectures
- Operations triggered by webhooks (which may arrive duplicated)
- Any operation where a duplicate causes business harm
- Public APIs where you donβt control the clientβs behavior
When to avoid
- Read-only operations (GET) that are already idempotent
- PUT/DELETE operations that are naturally idempotent
- Internal systems with reliable communication and no retries
- Prototypes where the added complexity isnβt justified
Common technologies and implementations
| Category | Options |
|---|---|
| Key storage | Redis (with TTL), PostgreSQL, DynamoDB |
| Standard headers | Idempotency-Key (Stripe), X-Request-Id, If-Match (ETags) |
| Libraries | Stripe SDK (built-in idempotency), custom middleware |
| Brokers with dedup | Kafka (exactly-once semantics), SQS (deduplication ID) |
| Frameworks | Express middleware, Spring interceptor, ASP.NET filter |
Relationship with other patterns
- Retry: Retries are safe only if the downstream is idempotent
- Event-Driven Architecture: Event consumers must be idempotent (at-least-once delivery)
- Outbox Pattern: Guarantees event publication, but the consumer still needs idempotency
- Saga Pattern: Each saga step must be idempotent to handle compensation retries
- Circuit Breaker: When the circuit closes and operations are retried, idempotency prevents duplicates
Next steps
Idempotency is a fundamental requirement for reliable distributed systems. To understand how to share models in a controlled way between services, explore the Shared Kernel. To see how idempotency applies in distributed transactions, review the Saga Pattern.