Design a Payment System, stage 6 of 13: break it
Write the idempotent checkout
Rewrite the handler. The client sends an Idempotency-Key header that it generates once per checkout attempt and reuses on every retry of that attempt. Use whatever mix of SQL and TypeScript you like. What matters is which operations are atomic, what is recorded when, and what each path returns.
System so far· 4 parts
Select a component to see what it is responsible for and which state it owns.
- 1Buyer's browser → Payment provider: Card entry and 3-D Secure in hosted fields
- 2Buyer's browser → Checkout API: Pay (Idempotency-Key header); poll status
- 3Checkout API → Postgres: Claim attempt; conditional transitions
What you need to know
The atomic claim in SQL:
INSERT INTO payment_attempts (order_id, idempotency_key, status) VALUES ($1, $2, 'processing') ON CONFLICT (idempotency_key) DO NOTHING RETURNING *;If a row comes back, this request created the attempt and should call the provider. If nothing comes back, another request already did, and this one should read and return that attempt's state.
Check
A second request with the same key arrives while the first is still waiting on the provider. What should it do?The same key can be reused by mistake for a different purchase: a bug that sends a cached key with a new order. Compare the stored attempt's order and amount with the request's, and reject a mismatch (422) rather than returning the old result.
Think first
The process crashes right after inserting the attempt and before calling the provider. What state is the system in, and how does it recover?