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

StorageAdvantagesDisadvantages
DatabasePersistent, transactionalSlower, requires cleanup
Redis/CacheFast, automatic TTLMay lose data on restart
Dedicated tableClear separation, easy to queryAdditional 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

CategoryOptions
Key storageRedis (with TTL), PostgreSQL, DynamoDB
Standard headersIdempotency-Key (Stripe), X-Request-Id, If-Match (ETags)
LibrariesStripe SDK (built-in idempotency), custom middleware
Brokers with dedupKafka (exactly-once semantics), SQS (deduplication ID)
FrameworksExpress 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.