Idempotency means an operation can be repeated without changing the result beyond the first successful application. In distributed systems, this is the difference between a safe retry and a duplicated business action.
At-least-once delivery, HTTP timeouts, webhook retries, and job replays all create ambiguity. The caller may not know whether the receiver failed before doing the work, after doing the work, or after sending a response. Idempotent design lets the caller retry without double-charging, double-shipping, or creating duplicate records.
Common techniques
- Idempotency keys. The client sends a stable key for a logical operation. The server stores the key and returns the original result for duplicate attempts.
- Message IDs. Consumers record event IDs before applying side effects. The inbox pattern is the durable version.
- Natural uniqueness. A database constraint on a business key prevents duplicate creation, for example one invoice per order.
- Conditional writes. State changes include the expected current state, so stale or repeated commands become no-ops.
- Upserts. The same input updates or creates the same row instead of appending another copy.
HTTP semantics
HTTP has method-level expectations, but business idempotency still depends on the implementation.
| Method | Idempotent by HTTP semantics | Notes |
|---|---|---|
GET |
Yes | Reads should not mutate business state |
PUT |
Yes | Replaces a known resource with the same representation |
DELETE |
Yes | Deleting an already deleted resource should not create a new effect |
POST |
No | Use an idempotency key for retried creates or commands |
PATCH |
Usually no | Can be idempotent if the patch sets values instead of incrementing them |
For APIs, the practical question is not only “can the server run this twice.” It is also “will the client see a stable outcome when it retries with the same key.”
Idempotency key flow
async function createCharge(request: ChargeRequest) {
const existing = await db.idempotency.findUnique({
where: { key: request.idempotencyKey },
});
if (existing) {
return existing.response;
}
return db.transaction(async (tx) => {
const charge = await tx.charge.create({
data: {
orderId: request.orderId,
amount: request.amount,
},
});
const response = { chargeId: charge.id, status: charge.status };
await tx.idempotency.create({
data: {
key: request.idempotencyKey,
response,
},
});
return response;
});
}
The key and the business write should commit together. If they are stored separately, a crash can leave the system in the exact ambiguity idempotency was meant to remove.
Design rules
- Scope keys to the operation and actor. A key like
abc123should not collide across tenants or endpoints. - Store enough response data to make retries stable.
- Set retention windows deliberately. Payment keys may need longer retention than analytics events.
- Return conflicts when the same key is reused with different parameters.
- Make downstream side effects idempotent as well. A safe API handler can still duplicate email, shipment, or ledger writes if those calls are not protected.
Trade-offs
Idempotency adds storage, indexes, and cleanup. It can also hide repeated user attempts if observability is weak. The cost is usually justified anywhere retries are automatic or money, inventory, access, or customer communication is involved.
