“In distributed systems, things will fail. Idempotency is how you survive that failure gracefully.”
1. What Is Idempotency?
In mathematics, an operation is idempotent if applying it multiple times produces the same result as applying it once. The formal definition:
f(f(x)) = f(x)
In REST API design, idempotency means:
Calling the same API endpoint with the same request payload multiple times should produce the same server-side state and (logically) the same response, no matter how many times it’s executed.
This is not just theory — it’s baked into the HTTP specification (RFC 9110). Here’s a breakdown of HTTP method idempotency:
| HTTP Method | Idempotent | Safe | Description |
|---|---|---|---|
GET
|
✅ Yes | ✅ Yes | Read-only, no side effects |
HEAD
|
✅ Yes | ✅ Yes | Like GET, no body |
OPTIONS
|
✅ Yes | ✅ Yes | Metadata retrieval |
PUT
|
✅ Yes | ❌ No | Full resource replace |
DELETE
|
✅ Yes | ❌ No | Remove resource |
POST
|
❌ No | ❌ No | Creates new resource (by default) |
PATCH
|
❌ No* | ❌ No | Partial update (context-dependent) |
PATCHcan be made idempotent by design (e.g.,SET quantity=5is idempotent;INCREMENT quantity by 1is not).
Safe vs. Idempotent — Don’t Confuse Them
- Safe = No server-side state mutation (read-only)
- Idempotent = Can be retried safely; same net effect on state
DELETE /orders/123
is idempotent (first call deletes; subsequent calls still result in “order 123
does not exist”) but NOT safe (it mutates state on first call).
2. The Problem This Solves
In modern microservices architectures, network calls fail. A lot. Consider this scenario:
Client ──→ API Gateway ──→ Order Service ──→ Payment Service
↓
(Network timeout!)
Did the payment go through?
The client receives a timeout error. It doesn’t know if:
- The request never reached the Payment Service
- The Payment Service processed it but the response was lost
- The request is still being processed
Without idempotency: Retrying creates duplicate payments. Customer is charged twice.
With idempotency: Retrying is safe. The server recognizes “I’ve seen this exact request before” and returns the original result without re-processing.
Real-World Consequences of Non-Idempotent APIs
- Double charges in payment systems (Stripe, PayPal lost millions before enforcing this)
- Duplicate orders in e-commerce platforms
- Ghost records in inventory systems during retry storms
- Data inconsistency across microservices during partial failures
- Cascading failures when upstream services retry without idempotency guards
Industry Standards That Enforce This
-
Stripe API
uses
Idempotency-Keyheaders for all mutation endpoints -
PayPal REST API
uses
PayPal-Request-Id - AWS S3 PUT operations are inherently idempotent
- Kubernetes uses resource version tokens for optimistic concurrency
3. Implementation Guide
Project Setup — Spring Boot Microservice
Step 1 — The Idempotency Key Model
Step 2 — Repository Layer
Step 3 — The Core Idempotency Service
Step 4 — The REST Controller (Order Service Example)
Step 5 — DTOs
Step 6 — Global Exception Handler
Step 7 — Scheduled Cleanup Job
Step 8 — Test Cases
Step 9 — application.yml Configuration
Architecture Diagram: Idempotency Flow
┌─────────────────────────────────────────────────────────┐
│ API Gateway / Load Balancer │
└──────────────────────────┬──────────────────────────────┘
│ POST /api/v1/orders
│ Idempotency-Key: abc-123
▼
┌─────────────────────────────────────────────────────────┐
│ OrderController │
│ Validates header + request body │
└──────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ IdempotencyService │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Check DB: Does key "abc-123" exist? │ │
│ └────────────────────┬─────────────────────────────┘ │
│ ┌───────────┼────────────────┐ │
│ YES│ │NO │PROCESSING │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ Check │ │ Insert key │ │ Return 409 │ │
│ │ Status │ │ PROCESSING │ │ Conflict │ │
│ └──────────┘ └──────┬──────┘ └──────────────┘ │
│ COMPLETED│ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌─────────────────────────┐ │
│ │ Return │ │ Execute Business Logic │ │
│ │ Cached │ │ (OrderService.create) │ │
│ │ Response │ └──────────┬──────────────┘ │
│ └──────────┘ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Cache response │ │
│ │ Mark COMPLETED │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
4. Pros & Cons
Advantages
1. Fault Tolerance and Retry Safety Distributed systems fail. Idempotent APIs let clients retry with confidence. Network timeouts, pod restarts, and transient errors no longer cause data corruption. This is foundational to building resilient microservices.
2. Simplifies Client Logic Without idempotency guarantees, clients must implement complex state tracking: “Did my request succeed? Should I retry? Will retrying cause duplicates?” With idempotency keys, the answer is always: “Just retry with the same key.”
3. Prevents Costly Business Logic Duplication In payment, order, and inventory systems, double-processing is a business disaster. Idempotency provides a technical guarantee that matches the business requirement: exactly-once execution semantics.
4. Aligns with HTTP Specification Properly implementing idempotency makes your API compliant with RFC 9110. This improves interoperability with HTTP clients, proxies, CDNs, and API gateways that may automatically retry safe requests.
Disadvantages
1. Storage Overhead and Cleanup Complexity Every unique request stores a record. High-volume APIs generate millions of idempotency records. You need TTL policies, scheduled cleanup jobs, and an indexed database table — adding operational complexity.
2. Not a Silver Bullet for Distributed Transactions Idempotency handles retry safety at the API layer. It does NOT solve distributed consistency across multiple services. If an order service is idempotent but the downstream inventory service is not, you still have problems. Each hop needs its own idempotency strategy.
3. Payload Mismatch Ambiguity What should happen when the same key arrives with a different payload? Is it a client bug or a legitimate re-use? You must define a clear contract (this implementation returns 422), but this edge case adds complexity to client implementations and error handling.
4. Concurrency Window and Race Conditions
Between the “check if key exists” and “insert key” operations, concurrent
requests can slip through without strict database-level uniqueness
constraints. Relying purely on application-level checks is insufficient — you
must enforce uniqueness at the database constraint level and handle
DataIntegrityViolationException
gracefully.
5. Top 5 Interview Questions & Answers
Q1. What is idempotency in REST APIs, and why does it matter in microservices?
Answer:
Idempotency means that performing the same operation multiple times produces the same result as performing it once, with no additional side effects. In REST, methods like GET, PUT, and DELETE are idempotent by specification. POST is not idempotent by default.
In microservices, this matters because distributed networks are inherently unreliable. When a client sends a request and receives a timeout, it cannot know whether the server processed the request. Without idempotency, retrying can lead to duplicate orders, double charges, or corrupted inventory counts. With idempotency keys, a client can safely retry any number of times, knowing the server will only execute the business logic once.
The implementation typically involves:
-
A client-generated UUID sent as a header (
Idempotency-Key) - Server-side persistence of the key and its response
- A lookup on every incoming request before execution
Q2. How would you implement idempotency for a POST endpoint that creates payments? What happens if two identical requests arrive simultaneously?
Answer (Scenario-Based):
For a payment endpoint, I’d implement the following strategy:
-
Require an
Idempotency-Keyheader (UUID) from the client on everyPOST /paymentscall. -
Before processing, query an
idempotency_recordstable using the key. - If key exists and COMPLETED, return the cached response — payment already processed.
-
If key exists and PROCESSING, return
409 Conflict— concurrent duplicate detected. -
If key is new, insert a record with
PROCESSINGstatus (using a unique DB constraint to handle race conditions atomically), process the payment, and update status toCOMPLETEDwith the serialized response.
For the simultaneous request scenario, the database unique constraint on
idempotency_key
column ensures only one insertion succeeds. The second concurrent thread gets
a
DataIntegrityViolationException, which I catch and translate into a
409 Conflict
response, telling the client to retry after a brief delay.
This is exactly how Stripe handles their payment API — their documentation
explicitly states that idempotency keys should be unique per logical operation
and that concurrent requests with the same key receive a
429
or
409
response.
Q3. Is PUT inherently idempotent? What about PATCH? Give examples.
Answer:
PUT is idempotent by HTTP specification. It performs a full resource replacement. For example:
PUT /users/42
{ "name": "Alice", "email": "alice@example.com" }
Calling this 10 times results in exactly the same state — user 42 always has name “Alice” and email “alice@example.com”. The first call creates/replaces the resource; subsequent calls replace it with the same data.
PATCH is NOT inherently idempotent — it depends on the operation semantics:
-
PATCH /accounts/1 { "balance": 500 }(SET operation) → Idempotent — always sets balance to 500 -
PATCH /accounts/1 { "balanceDelta": +100 }(INCREMENT operation) → NOT idempotent — balance grows with each call
For production APIs, I always design PATCH operations to be idempotent by using absolute values (SET semantics) rather than relative changes (INCREMENT semantics) for critical fields. If relative changes are required, I enforce idempotency keys on those endpoints.
Q4. How do you handle idempotency key expiration? What are the trade-offs?
Answer:
Idempotency keys should have a TTL — typically 24 to 48 hours. Here’s the reasoning and trade-offs:
Why expire keys?
-
Prevents unbounded growth of the
idempotency_recordstable - Old keys are no longer needed once the client has received a response and confirmed success
Implementation:
-
Store an
expires_attimestamp on each record -
Run a scheduled cleanup job (e.g., Spring
@Scheduled) to delete expired records - For high-scale systems, use Redis with native TTL support as a cache layer, backed by a database for durability
Trade-offs:
| TTL Duration | Pros | Cons |
|---|---|---|
| Very short (1 hour) | Smaller storage footprint | Client retries might fail if too slow |
| Medium (24 hours) | Balanced, industry standard (Stripe uses 24h) | Moderate storage growth |
| Long (7 days) | Very safe for slow clients | Significant storage cost at scale |
For payment systems, I align the TTL with the maximum expected retry window — usually 24 hours. For high-frequency, low-criticality APIs, shorter TTLs with Redis as the backing store are more cost-effective.
Q5. What’s the difference between idempotency and exactly-once delivery? Can you guarantee exactly-once execution in a distributed system?
Answer:
This is a subtle but critical distinction:
Idempotency is a property of an operation: calling it multiple times is equivalent to calling it once. It’s a guarantee about correctness when retried.
Exactly-once delivery is a messaging guarantee: a message is delivered to the consumer precisely once, no more, no fewer. It’s a guarantee about how many times a message is dispatched.
The key insight: you cannot guarantee exactly-once delivery in a distributed system (this is a well-established impossibility result under network partitions — see the CAP theorem and Two Generals Problem). Networks can duplicate messages; systems can crash and recover.
However, you can guarantee exactly-once execution by combining:
- At-least-once delivery (accept that retries will happen)
- Idempotent consumers (design your processing logic to be safe on duplicates)
This is the standard pattern in Apache Kafka (idempotent producers + transactional consumers), AWS SQS (at-least-once + idempotent handlers), and all production microservices at scale.
So in an interview context: the correct answer is “exactly-once delivery is a myth in distributed systems; we achieve exactly-once semantics through idempotent processing combined with at-least-once delivery.”
6. Conclusion
Idempotency is not a feature — it is a fundamental design contract that every production REST API must honor, especially in microservices architectures where partial failures are the norm, not the exception.
The implementation we walked through establishes a clean separation of concerns:
- The controller enforces the header contract at the HTTP boundary
- The idempotency service owns the lookup, locking, and caching logic
- The database constraint provides the atomic safety net for concurrent requests
- The cleanup scheduler manages operational hygiene
Beyond the code, the most important shift is conceptual: stop designing APIs that assume requests always succeed on the first attempt. Start designing APIs that assume requests will fail, be duplicated, and be retried — and make sure your system handles all of those scenarios correctly.
In the words of the distributed systems community: “Design for failure. Idempotency is how you make failure invisible to your users.”
The patterns in this guide align with how industry leaders like Stripe, Twilio, and AWS design their APIs. When you internalize idempotency as a default — not an afterthought — you will build systems that are resilient, trustworthy, and production-grade from day one.
Built with Java 21, Spring Boot 3.x, Spring Data JPA, PostgreSQL, and Spring Scheduler. All code follows industry best practices and is production-ready with appropriate modifications for your environment.
Post a Comment