Idempotency in REST APIs
Fast recall
Quick revision
Core answer
Whether you understand idempotency for retry safety and distributed systems
Saying DELETE is not idempotent because the second call returns 404 — the resource state is the same (deleted), so it is idempotent
Explain idempotency keys for POST operations (Stripe's approach for payment APIs)
Interview-ready guide
Interview answer
Idempotency means calling an operation multiple times has the same effect on server state as calling it once. It is the property that makes safe retries possible — critical for distributed systems where network failures and timeouts are normal.
Which methods are idempotent by spec
- GET, HEAD, OPTIONS — safe and idempotent (read-only, no side effects)
- PUT — idempotent:
PUT /orders/123 {"status": "shipped"}sets the same state every time - DELETE — idempotent: the resource is absent after the first call; subsequent calls may return
404but the state (resource absent) is consistent - POST — not idempotent by default; each call may create a new resource
Making POST idempotent
- The client generates a UUID and sends
Idempotency-Key:with the request - The server stores the result keyed by the idempotency key (typically in Redis with a TTL)
- Duplicate requests within the TTL window return the stored result without reprocessing
- Stripe uses this for payment operations — the client can retry safely on timeout without charging the customer twice
A PATCH operation's idempotency depends on the semantics: {"status": "active"} is idempotent (absolute assignment); {"balance": "+10"} is not (relative change).
Idempotency in REST APIs
Direct Answer
An operation is idempotent if calling it multiple times produces the same result as calling it once. GET, PUT, DELETE, and HEAD are idempotent. POST is not. PATCH may or may not be, depending on implementation.
Code Example
// Client sends a unique key per logical attempt; retries reuse the same key.
// curl example:
// curl -X POST https://api.example.com/v1/charges \
// -H 'Idempotency-Key: 7a9c3f12-b4e8-4e0a-9f77-0b3a8c2d1e55' \
// -H 'Content-Type: application/json' \
// -d '{"amount": 5000, "currency": "USD"}'
@RestController
@RequestMapping("/v1/charges")
class ChargeController {
private final ChargeService service;
private final IdempotencyStore store; // Redis or Postgres
@PostMapping
ResponseEntity<ChargeDto> create(
@RequestHeader("Idempotency-Key") @NotBlank String idemKey,
@RequestBody @Valid CreateChargeRequest req) {
// 1) Look up the key; hash the request body to detect reuse-with-different-body
String bodyHash = sha256(req);
Optional<IdempotentRecord> existing = store.find(idemKey);
if (existing.isPresent()) {
IdempotentRecord rec = existing.get();
// Same key + same body -> replay the stored response exactly
if (rec.requestHash().equals(bodyHash)) {
return ResponseEntity.status(rec.status())
.header("Idempotent-Replayed", "true")
.body(rec.response(ChargeDto.class));
}
// Same key + different body -> 409; this is a client bug we want to surface
throw new IdempotencyMismatchException(idemKey);
}
// 2) First request for this key — reserve a slot atomically (ms TTL until response known)
if (!store.tryReserve(idemKey, bodyHash, Duration.ofSeconds(30))) {
// Another in-flight request has the same key; 409 while it resolves
throw new IdempotencyInFlightException(idemKey);
}
// 3) Execute the real work
try {
ChargeDto result = service.createCharge(req);
store.commit(idemKey, bodyHash, 201, result, Duration.ofHours(24));
return ResponseEntity.status(HttpStatus.CREATED)
.header(HttpHeaders.LOCATION, "/v1/charges/" + result.id())
.body(result);
} catch (Exception ex) {
store.fail(idemKey, ex); // release the reservation so retries can succeed
throw ex;
}
}
}
// Redis-backed store sketch (atomic reservation via SET NX)
interface IdempotencyStore {
Optional<IdempotentRecord> find(String key);
boolean tryReserve(String key, String hash, Duration ttl);
void commit(String key, String hash, int status, Object body, Duration ttl);
void fail(String key, Throwable cause);
}
record IdempotentRecord(String requestHash, int status, String bodyJson, Instant at) {
<T> T response(Class<T> type) {
return JSON.readValue(bodyJson, type);
}
}
Common Mistakes & Pitfalls
Saying DELETE is not idempotent because the second call returns 404 — the resource state is the same (deleted), so it is idempotent
Check your understanding
Answer these without looking back at the article.
- Why is idempotency critical for payment APIs?
- Is PATCH idempotent? Give an example of each case.
- How would you implement an idempotency key store at scale?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.