Skip to content

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
123CLIENTBuyer's browserSERVICECheckout APIDATABASEPostgresEXTERNALPayment provider

Select a component to see what it is responsible for and which state it owns.

  1. 1Buyer's browser → Payment provider: Card entry and 3-D Secure in hosted fields
  2. 2Buyer's browser → Checkout API: Pay (Idempotency-Key header); poll status
  3. 3Checkout API → Postgres: Claim attempt; conditional transitions

What you need to know

0 of 2 checks done
  1. 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.

  2. Check

    A second request with the same key arrives while the first is still waiting on the provider. What should it do?