Case index E-commerce platform / 01 of 04

Claim

A recorded run of 20 concurrent checkouts against stock 5 created 5 orders and 15 rejections, leaving stock 0 and 5 open reservations.

A hosted storefront whose sandbox checkout moves an order to PAID through a signed Stripe webhook, with stock decided under concurrency.

(Next.js, TypeScript, PostgreSQL, Prisma, Stripe)

Live

Pay with the sandbox card 4242 4242 4242 4242 and watch the order move to PAID.

Demo sign-in demo@kestrel.test / demo-password-123

What to expect on the free instance

  • The free instance sleeps after 15 minutes idle and takes about a minute to wake.
  • Payments run in Stripe test mode with the 4242 card; no money moves.
  • The catalogue is seeded: 30 products, 4 categories and 22 brands.
  • The four shots follow the visitor path — catalogue, product, cart and checkout — so the admin console is not in the set.

Five-step walkthrough

01 / What it is

What it is

A seeded Next.js 15 storefront on PostgreSQL 16 with accounts, cart and admin. Checkout turns a cart into an order, reserves stock and creates a Stripe PaymentIntent from the total. A signed webhook moves the order to PAID, and a recorded replay answers 200 duplicate.

Sennheiser HD 598 product page: a headphone photograph on the left and a buy box with the 199.00 price, the Beige variant, In stock, quantity 1 and one Add to cart action on the right.
Sennheiser HD 598 product page: photograph on the left, and a buy box with 199.00, the Beige variant, In stock and Add to cart.
Cart with two lines at quantity 1 — Sennheiser HD 598 and Kestrel Pocket DAC — line totals, a server-computed 218.00 summary, free shipping and one Continue to checkout action.
Cart page with two lines at quantity 1, line totals, a server-computed 218.00 summary and one Continue to checkout action.
Checkout with the Stripe Payment Element mounted: card, Pay By Bank, Bancontact, MB WAY, Satispay and EPS options, a Pay 218.00 action, and the recalculated order summary beside it.
Checkout page with the Stripe Payment Element mounted, six payment methods, a Pay 218.00 action and the order summary.

02 / The hard part

The hard part

Stock, price and idempotency are decided under concurrency. Reading stock then writing a reservation lets concurrent buyers observe the same last unit; a retried submit can create a second order; a client can post a total the server did not compute. The database arbitrates each.

03 / The decision

The decision

The conditional UPDATE is the stock arbiter

Decision Decrement with UPDATE ... WHERE stock >= $qty inside the checkout transaction; a zero-row update aborts it as OUT_OF_STOCK.

Rejected alternative Read, check in application code, then write in a second statement; concurrent buyers can pass the read for the last unit.

Cost The loser pays for a rollback, and checkouts serialise on the variant row.

Evidence The recorded run returned 5 orders and 15 rejections on stock 5.

04 / The failure it refuses

The failure it refuses

Pathological case

Two buyers reach the last unit at the same moment, or a client posts its own total and retries the submit. The system resolves every such request to one order or an explicit refusal.

Mechanism Price is recomputed from the cart inside the checkout transaction; the conditional UPDATE decrements stock while stock >= qty and returns zero rows otherwise; the (userId, idempotencyKey) unique constraint folds a retry into the existing order. A zero-row update aborts the transaction as OUT_OF_STOCK.

05 / Measured

Measured

Recorded hosted payment: order KST-2610-53808809, a 14900-cent PaymentIntent and one signed webhook delivery.

Four invariants hold: a forged total is ignored; one order per (userId, idempotencyKey); 20 concurrent checkouts on stock 5 produced 5 orders and 15 rejections; a replayed webhook is a 200 no-op. The load run measured p95 4.58 s over 250 requests and 2 P2028 timeouts.

Measured results and the method behind each value.
Metric Value Method Artifact
Hosted sandbox payment order KST-2610-53808809 PAID · 14900 cents · replay 200 duplicate node scripts/live-payment-proof.mjs against the hosted store with Stripe test keys; order and webhook detail in docs/PROOF.md scripts/live-payment-proof.mjs
Checkout concurrency on stock 5 5 orders · 15 rejections · stock 0 · 5 open reservations npx vitest run tests/integration/checkout.oversell.test.ts against PostgreSQL 16; transcript in docs/proof/concurrency-run.txt tests/integration/checkout.oversell.test.ts
Checkout load, 50 virtual users p95 4.58 s · 250 checkout requests · 248 orders · 2 P2028 k6 run load/checkout.js, 50 virtual users x 5 iterations against a production next start; summary in docs/load/checkout.md load/checkout.js

Gate checks (test counts, not metrics)

  • Verification gate: 181 tests · 33 files · 5 Playwright tests. npm run verify (eslint, tsc, vitest, prisma migrate diff, Playwright) package.json

Invariants and their tests

Back to the case index