ContentsThe library

Designing an Interface to Call

The Second Call Is Not a Mistake

Last timeSaying What Went Wrong

A caller who never saw your answer has to ask again. Making that safe means a key they generate, a record you keep, and a decision about how long to keep it.

Why the second call happens

Somebody charges a card. Your service takes the request, charges the card,

writes the record, builds the response, and the connection drops before the

response arrives. The caller waits, times out, and sees nothing.

Now put yourself on their side. They know they sent a request. They do not

know whether it arrived. They do not know whether it was processed. A timeout

looks exactly the same whether the work happened or did not, and there is no

way for them to find out by looking at their own side of the wire.

They have two options and both are wrong. Give up, and a customer may have

been charged for an order that does not exist. Send it again, and a customer

may be charged twice. The second call is not a caller mistake. It is the only

remaining move for somebody who was denied an answer, and an interface that

punishes it is an interface that has pushed its own problem outwards.

FIG 1One payment, as each side saw it
stepmomentwhat the caller knewwhat the service knewthe customerwhat happened
1request sentwaitingnothing yetcard not chargedOrdinary. The caller is holding an open connection and a timer.
2card chargedstill waitingcharged, record writtencharged onceThe work is done and committed. Everything after this point is about the answer, not the work.
3connection dropstimeout, outcome unknownresponse sent successfullycharged onceThe service believes it succeeded. Its logs will say so. The caller has nothing.
4caller retriestrying againa second identical requestcharged twice, unlessThe last column is the whole subject of this lesson. Whether it says once or twice depends entirely on what the service does next.
4 steps
The divergence happens in the third row, and it is not caused by a failure of the work. The work was perfect. What failed was the report of the work, and no amount of care inside the operation addresses that.

A key the caller generates

The fix is to let the caller name the attempt. They generate a value before

they send anything, attach it to the request, and attach the same value to

every retry of that same intent. The service uses it to recognise the retry.

The important part is where the value comes from. It has to come from the

caller, and the reason is a question only the caller can answer: is this the

same intent as before, or a new one? Two requests to charge forty-two dollars

to the same card for the same order might be a retry of one payment or a

customer who genuinely bought the same thing twice in a minute. The contents

are identical. The intent is not, and nothing in the request distinguishes

them.

This rules out the tempting shortcut of hashing the request body and treating

matching hashes as duplicates. That design refuses the legitimate second

order, which is a worse failure than the one it fixes, because it is silent

and the customer is simply told no.

FIG 2The key, and the four answers to it
plaintext
POST /payments
Idempotency-Key: 8c1f0e5a-5a3d-4e71-9b24-2f8d6a0b7c33
{amount: 4200, currency: cad, order: ord_7781}

first call        201 Created, payment pay_4Q2k, key recorded
retry, same body  200 OK, payment pay_4Q2k, replayed from the record
retry, in flight  409 Conflict, the first call has not finished
same key, new body  422 Unprocessable, that key means something else

recorded against the key
  key             the value the caller sent
  request_digest  a hash of method, path and body
  state           in_progress or complete
  status_code     201
  response_body   the whole first answer, byte for byte
  created_at      2026-10-09T14:12:08Z
The first four lines are what the caller sends. The next four are the only outcomes the service ever needs. The last block is what has to exist in storage for those outcomes to be possible, and it is more than most first attempts store.

Two properties make a key work. It must be unique per intent, which in

practice means a random identifier rather than anything derived from the

contents. And it must be generated before the first attempt and held by the

caller across retries, which means it has to survive whatever made the retry

necessary, including a process restart on their side.

The lesson stops here

4 more paragraphs to go

You have read the opening. The rest of the argument, the problems that check whether it landed, and the lines worth keeping at the end all come with a plan.

The first lesson of every course in the library reads the whole way through, free, so you can see exactly what the rest of them are.

See the planThe contents

This is the reading half

Starting the course gives you your own copy of it. Every idea on every page has problems standing under it, marked with a reason rather than a tick, and any sentence you do not believe can be opened and argued with. None of that can happen on a page nobody owns.

The contents

The rest of this course

  1. 01Every Awkward Interface You Have Ever Used Was Awkward for the Same Reason, and the Reason Was Decided in the First Half Hour
  2. 02There Are Only Two Questions Worth Asking About Any Operation, and Neither of Them Is What It Is Calledopening only
  3. 03Your Error Message Is Read by a Program First and a Human Second, and Almost Every Interface Gets That Order Backwardsopening only
  4. 04The Second Call Is Not a Mistakeyou are here
  5. 05Counting From the Start Versus Remembering the Placeopening only
  6. 06A Limit Is Only Useful If Somebody Can Obey Itopening only
  7. 07Hand Back a Receipt, Not an Answeropening only
  8. 08Add, Migrate, Then Removeopening only

Read alongside