The Power of Idempotency in APIs
Ever retried an API call and ended up with duplicate orders, multiple charges, or a corrupted state? That’s what happens when APIs aren’t idempotent.
What is Idempotency?
In simple terms:
An idempotent operation produces the same result no matter how many times it is executed.
DELETE /users/123
→ Delete the user with ID 123. Calling it once or five times results in the same outcome: the user is gone.GET /users/123
→ Fetch user details. You can call this 1000 times, and nothing changes on the server.POST /orders
→ Without precautions, this might create multiple orders if retried — and this is where idempotency matters most.
In APIs, especially when dealing with payments, orders, or resource creation, idempotency ensures that retries don’t break your system or annoy users.
Why Does Idempotency Matter?
Let’s look at real-world scenarios:
- Payment Systems
Imagine a user clicks “Pay” but their internet lags. The client retries the request. Without idempotency, you might charge them twice. - Order Placement
On e-commerce platforms, retries could lead to multiple shipments of the same item. That’s a costly mistake. - Microservices Communication
In distributed systems, network timeouts are common. If service A retries a request to service B, you must ensure B doesn’t execute the action twice.
Bottom line: Retries are inevitable. Duplicates are avoidable.
How to Implement Idempotency
1. Use HTTP Semantics Correctly
- GET, PUT, DELETE → Should already be idempotent.
- POST → Not inherently idempotent, so you must enforce it.
👉 Rule of thumb: If your POST
creates a resource, make sure clients can safely retry without creating duplicates.
2. The Idempotency Key
One of the most common approaches is to use an Idempotency Key — a unique identifier (usually a UUID) provided by the client with every request.
- If the server sees the same key again, it should return the same result instead of performing the action again.
Example:
@PostMapping("/payments")
public ResponseEntity<PaymentResponse> createPayment(
@RequestBody PaymentRequest request,
@RequestHeader("Idempotency-Key") String idempotencyKey) {
// Check if this key was already used
Optional<PaymentResponse> existing = paymentService.findByIdempotencyKey(idempotencyKey);
if (existing.isPresent()) {
return ResponseEntity.ok(existing.get());
}
// Process the payment only once
PaymentResponse response = paymentService.processPayment(request);
// Save response linked to the idempotency key
paymentService.saveWithIdempotencyKey(idempotencyKey, response);
return ResponseEntity.ok(response);
}
Here:
- The first request processes the payment.
- Subsequent retries with the same key return the saved response.
This is exactly how Stripe’s payment API avoids double charges.
3. PUT vs POST Example
Let’s say you want to update a user’s profile picture.
POST /users/123/profile-pic
→ If retried, might upload multiple pictures.PUT /users/123/profile-pic
→ Safer, because PUT is replace if exists, making it idempotent.
Java Example:
@PutMapping("/users/{id}/profile-pic")
public ResponseEntity<?> updateProfilePic(@PathVariable String id,
@RequestParam("file") MultipartFile file) {
userService.updateProfilePic(id, file);
return ResponseEntity.ok("Profile picture updated");
}
No matter how many times this is retried, the end state is one profile picture.
4. Handling Edge Cases
- Expired Idempotency Keys: Don’t store them forever; decide a reasonable TTL (e.g., 24 hours).
- Different Payloads with Same Key: Reject such requests — otherwise, you’ll break consistency.
if (existing.isPresent() && !existing.get().getRequest().equals(request)) {
throw new IllegalStateException("Idempotency key already used with different request");
}
Real-World Example: Food Delivery App
Imagine you’re building a food ordering app:
- Customer clicks “Place Order” →
POST /orders
with idempotency keyabc-123
. - App crashes mid-response.
- Customer reopens app; it retries with the same key.
- Server sees key
abc-123
→ returns the existing order instead of creating a new one.
This avoids:
- Duplicate food delivery.
- Double billing.
- Confused customers.
The Cost of Ignoring Idempotency
Without idempotency:
- Payments might get double charged.
- Orders might get duplicated.
- Support tickets pile up (“I was billed twice!”).
- Engineers waste time debugging issues caused by retries instead of writing features.
With idempotency:
- Systems are resilient to retries.
- Customers trust your product.
- Support costs drop.
- Your team sleeps better at night.
Final Thoughts
Idempotency isn’t just a “nice to have.” It’s essential for building APIs that behave reliably under real-world conditions.
If your API deals with money, orders, or state changes, enforcing idempotency is one of the best investments you can make.
👉 Next time you design an API, ask yourself:
“What happens if this request is retried 5 times?”
If the answer is “things break”, then you’ve got work to do.
✨ If you found this guide valuable and you’d like more practical dev learnings like this, follow us on X (@theDevLearning) where we share daily coding lessons from real-world experience.
💡 Want to show support? A retweet or share of this post helps the newsletter reach more developers like you.
Member discussion