Claim
A recorded run of 20 concurrent checkouts against stock 5 created 5 orders and 15 rejections, leaving stock 0 and 5 open reservations.
Search, authentication, cart and Stripe test checkout with server-side price validation.
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 records a payment webhook. The PaymentIntent is a locally generated pi_demo_* value recorded by the webhook replay.
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
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
Four checkout invariants hold under test: a forged total is ignored; one order exists per (userId, idempotencyKey); 20 concurrent checkouts on stock 5 produced 5 orders and 15 rejections; a replayed webhook is a 200 no-op. The gate run recorded 181 tests across 33 files and 5 Playwright tests.
The load run measured p95 4.58 s over 250 checkout requests, with 2 P2028 timeouts.
| Metric | Value | Method | Artifact |
|---|---|---|---|
| Checkout latency, 50 concurrent virtual users | p50 2000 ms · p95 2660 ms · p99 2880 ms | k6 load/checkout.js against localhost:3000 on this Windows 11 machine, 50 VUs, 250 checkouts in a 1-minute run | docs/load/checkout.md |
| Orders created by 20 concurrent checkouts on stock 5 | 5 orders · 15 rejected · stock 0 · 5 open reservations | npx vitest run tests/integration/checkout.oversell.test.ts, real Postgres on shop_test | docs/proof/concurrency-run.txt |
| Hub screenshot set | 4 images · 2880x1800 · 291-470 KB each | npm run assets (Playwright at 1440x900 CSS px, deviceScaleFactor 2) against npm run build && npm run start after npx prisma db seed -- --seed 42 | docs/proof/screenshots.txt |
| Verification gate | exit 0: eslint, tsc, vitest, prisma diff, Playwright | npm run verify on this Windows 11 machine | docs/proof/verify.txt |
Invariants and their tests
- A forged price in the checkout body cannot change the charge
tests/integration/checkout.forged-price.test.ts - One order per (userId, idempotencyKey) under concurrent double submit
tests/integration/checkout.double-submit.test.ts - 20 concurrent checkouts on stock 5 create exactly 5 orders and stock never goes negative
tests/integration/checkout.oversell.test.ts - An amount mismatch becomes REFUND_REQUIRED, never PAID
tests/integration/checkout.amount-mismatch.test.ts - A replayed Stripe webhook is a 200 no-op
tests/integration/webhook.replay.test.ts - Expired reservations release stock and cancel the order
tests/integration/reservations.sweeper.test.ts