An operation is idempotent if repeating it has the same effect as doing it once. Networks time out and clients retry, so a "create payment" call must not charge twice. Use a client-supplied idempotency key enforced by a unique constraint.
CREATE TABLE payment (
id bigserial PRIMARY KEY,
idempotency_key text NOT NULL UNIQUE,
amount numeric(12,2) NOT NULL,
status text NOT NULL,
response jsonb
);
INSERT INTO payment (idempotency_key, amount, status)
VALUES ('k-123', 50.00, 'PENDING')
ON CONFLICT (idempotency_key) DO NOTHING; -- 0 rows => replay: return stored response
- Insert the key and perform the side effect in the same transaction where possible; for external calls (PSP) pass the key downstream too.
- Store the response so a retry returns the same result; reject same key with a different payload (422).
- Expire keys after a window (e.g. 24 h).
- MySQL equivalent:
INSERT ... ON DUPLICATE KEY UPDATE or INSERT IGNORE.
PUT and DELETE are idempotent by definition; POST is not and needs the key.
⚠ Follow-up traps
- Why not check
SELECT first, then insert? Two concurrent retries both see nothing; the unique constraint is the only race-free guard.
- What if the first attempt is still in progress? Return 409/processing based on the
PENDING row rather than re-executing.
#idempotency#unique-constraint#api-design