All articlesShoaib
Technical deep dive//5 min read

How I Design Idempotent APIs for Reliable Processing

Retries are inevitable; duplicate business effects should not be.

APIsReliabilityTransactions
How I Design Idempotent APIs for Reliable Processing

Assume the network will lie

A client can send a request successfully while receiving a timeout. The user retries, the worker retries, or a load balancer retries. Any write API that assumes one request equals one attempt is eventually going to create a duplicate.

An idempotency key gives the operation an identity that survives transport retries. The server stores the result alongside the key and returns the same outcome for a repeated request.

The details matter

Keys need an owner, an expiry policy, and a payload consistency check. Reusing a key for a different body should be an error, not a lucky overwrite.

The database transaction must cover both the business effect and the idempotency record. Otherwise the system can remember a request without applying it, or apply it without remembering it.

Reliable APIs make retries boring. That is the point.

Two layers of protection

An idempotency key protects the application operation. A database constraint protects the business invariant. The key prevents the same request from being applied twice; the constraint prevents two different requests from creating an impossible state.

sequenceDiagram
  participant C as Client
  participant A as API
  participant D as Database
  C->>A: POST with Idempotency-Key
  A->>D: Begin transaction
  D-->>A: No prior key
  A->>D: Apply effect and store result
  D-->>A: Commit
  A-->>C: Result
  C->>A: Retry with same key
  A->>D: Find stored result
  D-->>A: Original result
  A-->>C: Same result

What to store

For each key, I store the authenticated actor, a request fingerprint, the final status, and the response body or a durable reference to it. A key reused with a different payload should return a conflict. Retaining only a boolean is not enough because the client needs the original result after a timeout.

const result = await database.transaction(async (tx) => {
  const existing = await tx.idempotency.find(key);
  if (existing) return existing.response;

  const created = await tx.orders.create(input);
  return tx.idempotency.store(key, {
    requestHash: hash(input),
    response: created,
  });
});

Expiration and concurrency

Keys need an expiry window that matches the operation. A payment may need longer protection than a UI preference update. Concurrent requests with the same key must contend on a unique database index or lock; checking for an existing key and inserting later leaves a race.

Idempotency is not a license to make every endpoint repeatable. It is a precise contract that says what repeated intent means for one operation.