Idempotency
Running the same operation twice leaves things exactly as running it once did. It makes retries safe instead of scary.
See it
What it is
An operation is idempotent when doing it five times lands you in the same place as doing it once. Deleting a file is idempotent (it stays deleted). Charging a card is not: two calls, two charges. This is why HTTP treats GET, PUT, and DELETE as idempotent and POST as not, and why the browser warns you before resubmitting a form.
You need it anywhere a request can arrive twice, which is everywhere: impatient double clicks, flaky networks where the response was lost but the write succeeded, webhook senders that retry, and job queues that promise at-least-once delivery. The standard fix is an idempotency key: the client generates a unique id (a UUID) per logical action and sends it with the request. The server stores it with a unique constraint, and on a repeat it returns the saved result instead of redoing the work. Stripe's API popularized this pattern with its Idempotency-Key header.
The misconception: 'my endpoint returns 200 on a retry, so it's idempotent'. Swallowing the second call is not enough if it already created a second row. Store the key inside the same transaction as the write, or the gap between 'record the key' and 'do the work' is a race condition. Also scope keys per user and expire them (24 hours is typical), or your idempotency table becomes the biggest one you own.
Ask AI for it
Make the [ENDPOINT / job handler] idempotent using an idempotency key. Accept a client-supplied key (Idempotency-Key header or a request field, a UUID per logical action). Create an idempotency_keys table with a unique constraint on (user_id, key) storing the key, request fingerprint, status ('in_progress' or 'completed'), response status code, response body, and created_at. On each request, open a transaction and claim the key with INSERT ... ON CONFLICT DO NOTHING, then check whether the insert claimed a row rather than letting a unique violation abort the transaction. If I claimed it, do the actual write and save the response and 'completed' status in that same transaction. If I did not claim it, the other request owns it: read the row, and if it is still 'in_progress', wait for the owning transaction to resolve (SELECT ... FOR UPDATE on that row, or a short bounded poll) rather than guessing, then replay its committed response verbatim with the original status code, or return 409 only once the wait times out. Reject a reused key whose request fingerprint does not match with a 422. Expire keys after 24 hours with a cleanup job. Add tests that fire the same request twice concurrently and assert exactly one row was created and both callers got the same response body.