Definition
- Idempotency – Performing the same operation multiple times produces the same result as performing it once; repeated requests do not change state beyond the first.
- State vs response – Idempotency is about the resulting state, not the response being byte-identical; a second DELETE returning 404 is still idempotent.
- Idempotent methods – GET, HEAD, PUT, DELETE, OPTIONS are defined as idempotent by the HTTP specification.
- Non-idempotent methods – POST and PATCH are not inherently idempotent; two identical POSTs usually create two resources.
- Idempotency key – A client-generated unique key per logical operation that the server uses to detect and replay retries.
Idempotency is like Rajesh telling the bank "set my standing instruction to pay 10000 rent". No matter how many times he repeats that exact instruction, the standing instruction is still 10000, so repeating it is harmless. But "transfer 10000 to Priya" is different: say it three times and Priya gets 30000. The first kind of instruction is idempotent; the second is not, and that is why a retried transfer is dangerous.
Understanding the Concepts
What is idempotency precisely?
The definition is about state, not about the response being byte-identical. An operation is idempotent if the resulting state after one call is the same as after many identical calls.
package com.bank.idempotency;
import java.math.BigDecimal;
public class IdempotencyDefinition {
public static void main(String[] args) {
// Idempotent: SET balance = 1000000
BigDecimal balance = new BigDecimal("1000000.00");
balance = new BigDecimal("1000000.00"); // same state
balance = new BigDecimal("1000000.00"); // still same state
System.out.println("After 3 SET operations: " + balance);
// NOT idempotent: balance = balance + 10000
BigDecimal bal2 = new BigDecimal("1000000.00");
bal2 = bal2.add(new BigDecimal("10000.00")); // 1010000
bal2 = bal2.add(new BigDecimal("10000.00")); // 1020000
bal2 = bal2.add(new BigDecimal("10000.00")); // 1030000
System.out.println("After 3 ADD operations: " + bal2);
}
}
Expected output:
After 3 SET operations: 1000000.00
After 3 ADD operations: 1030000.00
Which HTTP methods are idempotent?
GET and HEAD only read. PUT replaces a resource. DELETE leaves it deleted. OPTIONS describes capabilities. POST creates or processes, so two identical POSTs usually do the thing twice.
| Method | Idempotent | Safe | Why |
|---|---|---|---|
| GET | Yes | Yes | Only reads, no state change |
| HEAD | Yes | Yes | Only reads headers |
| PUT | Yes | No | Replaces resource with given value |
| DELETE | Yes | No | Deleting already-deleted is still deleted |
| OPTIONS | Yes | Yes | Describes capabilities |
| POST | No | No | Creates new resource or runs a command |
| PATCH | No | No | Relative update can change state each time |
package com.bank.idempotency;
public class HttpMethodIdempotency {
public static void main(String[] args) {
System.out.println("PUT /accounts/SAV2001 {balance: 1000000}");
System.out.println(" Call 1: sets balance to 1000000");
System.out.println(" Call 2: sets balance to 1000000 (same state)");
System.out.println(" -> Idempotent: state unchanged after repeat");
System.out.println();
System.out.println("DELETE /accounts/SAV2001");
System.out.println(" Call 1: deletes account, returns 204");
System.out.println(" Call 2: account already gone, returns 404");
System.out.println(" -> Idempotent: state is 'deleted' either way");
System.out.println();
System.out.println("POST /transfers {from: SAV2001, to: SAV2002, amount: 10000}");
System.out.println(" Call 1: transfers 10000");
System.out.println(" Call 2: transfers 10000 AGAIN");
System.out.println(" -> NOT idempotent: Priya gets 20000 instead of 10000");
}
}
Expected output:
PUT /accounts/SAV2001 {balance: 1000000}
Call 1: sets balance to 1000000
Call 2: sets balance to 1000000 (same state)
-> Idempotent: state unchanged after repeat
DELETE /accounts/SAV2001
Call 1: deletes account, returns 204
Call 2: account already gone, returns 404
-> Idempotent: state is 'deleted' either way
POST /transfers {from: SAV2001, to: SAV2002, amount: 10000}
Call 1: transfers 10000
Call 2: transfers 10000 AGAIN
-> NOT idempotent: Priya gets 20000 instead of 10000
Why does idempotency matter in microservices?
The network is unreliable at every layer. Clients time out and retry. Load balancers retry idempotent methods automatically. Mobile apps on flaky connections resend.
package com.bank.idempotency;
public class WhyItMatters {
public static void main(String[] args) {
System.out.println("Retry sources in a microservice system:");
System.out.println(" 1. Client timeout -> mobile app resends request");
System.out.println(" 2. Load balancer -> retries GET/PUT on connection failure");
System.out.println(" 3. API gateway -> retries on 503");
System.out.println(" 4. Kafka consumer -> at-least-once redelivery");
System.out.println(" 5. Saga orchestrator -> retries compensating transactions");
System.out.println();
System.out.println("If the operation is idempotent: all retries are harmless");
System.out.println("If NOT: double charge, duplicate loan, double interest");
}
}
Expected output:
Retry sources in a microservice system:
1. Client timeout -> mobile app resends request
2. Load balancer -> retries GET/PUT on connection failure
3. API gateway -> retries on 503
4. Kafka consumer -> at-least-once redelivery
5. Saga orchestrator -> retries compensating transactions
If the operation is idempotent: all retries are harmless
If NOT: double charge, duplicate loan, double interest
How do you make POST idempotent with an idempotency key?
The client generates a unique key per logical operation. The server stores the key with the first result. A retry with the same key returns the stored result.
package com.bank.idempotency;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class IdempotentTransferService {
private final Map<String, TransferResult> idempotencyStore = new ConcurrentHashMap<>();
public TransferResult transfer(String idempotencyKey, String from, String to, BigDecimal amount) {
// Check if this key was already processed
TransferResult existing = idempotencyStore.get(idempotencyKey);
if (existing != null) {
System.out.println("Idempotency key " + idempotencyKey + " already processed, replaying");
return existing;
}
// Process the transfer
TransferResult result = new TransferResult("TXN-001", "COMPLETED", amount);
idempotencyStore.put(idempotencyKey, result);
System.out.println("Transfer processed: " + amount + " from " + from + " to " + to);
return result;
}
record TransferResult(String txnId, String status, BigDecimal amount) {}
}
Expected output (first call):
Transfer processed: 50000.00 from SAV2001 to SAV2002
Expected output (retry with same key):
Idempotency key IK-20240115-001 already processed, replaying
What is the common mistake with GET?
Hiding a non-idempotent side effect behind GET is a serious bug because intermediaries are allowed to retry GET freely.
package com.bank.idempotency;
public class GetSideEffectBug {
// WRONG: GET that computes and persists interest
// GET /accounts/SAV2001/interest
// -> calculates interest and SAVES it to the database
// Load balancer retries on timeout -> interest posted TWICE
// CORRECT: GET should only READ
// POST /accounts/SAV2001/interest/compute
// -> calculates and saves interest
// GET /accounts/SAV2001/interest
// -> reads the computed interest (safe to retry)
public static void main(String[] args) {
System.out.println("WRONG: GET /interest that writes to DB");
System.out.println(" Load balancer retries -> interest posted twice");
System.out.println();
System.out.println("CORRECT: POST to compute, GET to read");
System.out.println(" POST is protected with idempotency key");
System.out.println(" GET only reads -> safe to retry any number of times");
}
}
Expected output:
WRONG: GET /interest that writes to DB
Load balancer retries -> interest posted twice
CORRECT: POST to compute, GET to read
POST is protected with idempotency key
GET only reads -> safe to retry any number of times
How does PATCH differ from PUT?
PUT replaces the entire resource (idempotent). PATCH applies a partial update, which may or may not be idempotent depending on the operation.
package com.bank.idempotency;
public class PutVsPatch {
public static void main(String[] args) {
System.out.println("PUT /accounts/SAV2001 {balance: 1000000}");
System.out.println(" Always sets balance to 1000000 -> idempotent");
System.out.println();
System.out.println("PATCH /accounts/SAV2001 {operation: 'increment', amount: 100}");
System.out.println(" Call 1: balance + 100 = 1000100");
System.out.println(" Call 2: balance + 100 = 1000200");
System.out.println(" -> NOT idempotent: different state on each call");
System.out.println();
System.out.println("PATCH can be idempotent if body is an absolute value:");
System.out.println(" PATCH /accounts/SAV2001 {email: 'rajesh@nbi.com'}");
System.out.println(" -> Sets email to same value each time -> idempotent");
}
}
Expected output:
PUT /accounts/SAV2001 {balance: 1000000}
Always sets balance to 1000000 -> idempotent
PATCH /accounts/SAV2001 {operation: 'increment', amount: 100}
Call 1: balance + 100 = 1000100
Call 2: balance + 100 = 1000200
-> NOT idempotent: different state on each call
PATCH can be idempotent if body is an absolute value:
PATCH /accounts/SAV2001 {email: 'rajesh@nbi.com'}
-> Sets email to same value each time -> idempotent
What about idempotency in async consumers?
At-least-once delivery in Kafka is the messaging equivalent of a client retry. Consumers must be idempotent too.
package com.bank.idempotency;
public class AsyncIdempotency {
public static void main(String[] args) {
System.out.println("HTTP retry: client sends same request twice");
System.out.println("Kafka retry: broker delivers same message twice");
System.out.println();
System.out.println("Same problem, same solution:");
System.out.println(" 1. Check if message_id already processed");
System.out.println(" 2. If yes: skip and acknowledge");
System.out.println(" 3. If no: process, save message_id, acknowledge");
}
}
Expected output:
HTTP retry: client sends same request twice
Kafka retry: broker delivers same message twice
Same problem, same solution:
1. Check if message_id already processed
2. If yes: skip and acknowledge
3. If no: process, save message_id, acknowledge
What should a senior engineer know?
Model true replacements as PUT, keep reads side-effect-free, protect non-idempotent commands with an idempotency key, and apply the same discipline to async consumers.
Senior guidelines:
- Model updates as PUT (replace) when possible -> naturally idempotent
- Never hide writes behind GET -> load balancers retry GET freely
- POST for commands that create or transfer -> add idempotency key
- Idempotency key scope: per logical operation, not per HTTP attempt
- Key store: Redis with TTL or DB with unique constraint
- Handle concurrent same-key: atomic claim, wait or 409 for second
- Async consumers: same idempotency check for at-least-once delivery
Visual Overview
Idempotency Key Flow:
Client Server
│ │
│ POST /transfers │
│ Idempotency-Key: IK-001 │
│ {from: SAV2001, ...} │
│────────────────────────▶│
│ │ Key IK-001 not found
│ │ -> Process transfer
│ │ -> Store result against IK-001
│◀────────────────────────│ 201 Created {txnId: TXN-001}
│ │
│ (timeout, retry) │
│ │
│ POST /transfers │
│ Idempotency-Key: IK-001 │
│ {from: SAV2001, ...} │
│────────────────────────▶│
│ │ Key IK-001 FOUND
│ │ -> Return stored result
│ │ -> NO second transfer
│◀────────────────────────│ 200 OK {txnId: TXN-001}
Key Takeaway
Idempotency means performing the same operation many times leaves the system in the same state as performing it once. The subtlety is that this is about state, not response bytes: a second DELETE returning 404 instead of 204 is still idempotent because the resource is equally gone. GET, HEAD, PUT, DELETE, and OPTIONS are idempotent by HTTP specification. POST and PATCH are not, because two identical POSTs usually create two resources or run two transfers.
This matters because the network is unreliable at every layer: clients time out and retry, mobile apps on flaky connections resend, load balancers automatically retry idempotent methods on connection failure, and Kafka delivers at least once. If the operation is idempotent, all these retries are harmless. If not, a retry double-charges Rajesh or creates a duplicate loan application. The method semantics are a contract that intermediaries rely on, which is why hiding a money-moving side effect behind GET is a serious bug.
At National Bank of India, payments and transfers are fundamentally POST operations, which are not idempotent. Pankaj closes that gap with an idempotency key: the client sends a unique key per logical operation, the server stores the key with the first result, and any retry with the same key replays the stored result. He also applies the same discipline to Kafka consumers, since at-least-once delivery is the messaging equivalent of a client retry.
Remember one sentence: idempotency means repeating a request yields the same end state, GET/PUT/DELETE are naturally idempotent while POST is not, and since unreliable networks cause retries at every layer, protect non-idempotent commands with an idempotency key.
---