Back to Data Management & Consistency
Q8Question 8 of 10

What is idempotency, which HTTP methods should be idempotent, and why does it matter

Key Concepts
  • Idempotency means repeating a request gives the same end state as doing it once; it is about state, not response bytes.
  • GET, HEAD, PUT, DELETE, and OPTIONS are idempotent by HTTP specification; POST and PATCH are not inherently idempotent.
  • Networks are unreliable: clients, proxies, and load balancers retry requests, so non-idempotent retries can double-charge.
  • PUT replaces a resource and is naturally idempotent; DELETE leaves a resource deleted regardless of repetitions.
  • POST creates or processes, so two identical POSTs usually act twice: two transfers, two loan applications.
  • Close the POST gap with an idempotency key: client sends a unique key, server stores the result, retry replays it.
  • Apply the same discipline to async consumers: at-least-once delivery is the messaging equivalent of a client retry.

Definition

Idempotency means repeating a request gives the same end state as doing it once, GET/PUT/DELETE/HEAD/OPTIONS are idempotent while POST/PATCH are not, and it matters because unreliable networks cause retries that must not double-apply.
  • 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.

JAVA
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);
    }
}
TEXT
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 |

JAVA
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");
    }
}
TEXT
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.

JAVA
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");
    }
}
TEXT
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.

JAVA
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) {}
}
TEXT
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.

JAVA
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");
    }
}
TEXT
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.

JAVA
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");
    }
}
TEXT
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.

JAVA
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");
    }
}
TEXT
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.

TEXT
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate