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.

Search, authentication, cart and Stripe test checkout with server-side price validation.

(Next.js, TypeScript, PostgreSQL, Stripe)

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.

Headphones category at 1440x900: brand, price and availability filters in the left rail, a four-column product grid with ink prices, and a sort control.
Headphones category beneath a header with no notice bar, with brand and price filters, a four-column grid of pale product tiles and from-prices.
Cart table with Item, Price, Quantity and Total headers, two seeded lines at quantity 2 and 1, a server-computed order summary, and one accent checkout action.
Cart with two seeded lines at quantity 2 and 1, line totals, a server-computed summary and one teal checkout action.
Admin orders console showing the two seeded orders with customer, total, status and age columns, and a single accent Filter action.
Admin orders console with two seeded orders, customer, total, status and age columns, and a teal Filter action.

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

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.

Measured results and the method behind each value.
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

Back to the case index