Engineering note · 04 Commerce & reliability

Designing commerce workflows
that survive retries and race conditions.

Eight simultaneous orders for the last unit of a product all succeeded. This is how the AP Enterprises orders, payments and stock code was made to hold up when requests repeat or arrive at the same time—and how tests proved it.

Topic

Commerce & reliability · Concurrency, idempotency, stock integrity

Grounded in

The AP Enterprises platform, tested and fixed in October 2026

Published


Updated

Reading time

4 min read

The problem 01

Correct one request at a time isn't correct.

A commerce platform coordinates stock, payments, refunds and orders, and any of them can be touched by more than one request at once. Two customers tap "buy" on the last unit. A phone on a bad network sends the same refund twice. A staff member dispatches an order in the same second the customer cancels it. None of these are edge cases; they're what real traffic looks like.

The AP Enterprises code handled each of these correctly when requests came one at a time. Then a new test suite started firing them together against a real Postgres database. Eight simultaneous orders for one unit in stock: all eight succeeded. Two ₹300 refunds on a ₹500 payment: both went through. A double-submitted return refund paid out twice. Two cancels at once put the stock back twice. Every one of these looked fine in code review.

The mechanisms 02

Request→Idempotency key→Row lock→Stock + ledger, one transaction→Commit

Let the database decide, one writer at a time.

Stock now moves inside the order's own transaction, so it changes if and only if the order exists. Updating the balance locks that product's stock row until the order commits: concurrent orders queue behind it, and the database's "never below zero" check sees every earlier sale. Orders touching several products lock them in a fixed order, so two orders can't deadlock each other. Every movement is also written to an append-only ledger in the same transaction, so the balance and its history can't disagree.

The same rule fixed the other races. An order's status change locks the order row, so a cancel racing a dispatch has exactly one winner. A cancelled order's stock goes back by reversing its own ledger rows, so a second restock finds nothing left to return. Every refund that hasn't failed, including one still in flight, counts against what's left to refund. Adding to a cart locks the cart. Refresh tokens and one-time codes are used up by a single conditional update, so checking a token and spending it happen in one step that can't interleave.

Retries get their own mechanism. The three requests that move money—starting a payment, choosing cash on delivery, and issuing a refund—require an idempotency key. A retry with the same key returns the stored response without a second gateway call; a duplicate still in flight gets a 409; the same key with a different body is rejected as a client bug. Keys are tied to the caller: until October 2026, another user sending the same key got the first user's stored response back. The payment gateway's own retries are de-duplicated separately, by its event ID.

PostgreSQLRow locksIdempotency keysStock ledgerGo backendReal-database tests

Key decisions 03

Stock inside the order transaction

Deduct stock in the same transaction that creates the order, under a row lock, instead of after the order commits.

Trade-off: orders for one popular product line up behind a single row.
A paid order is never refused

A cash-on-delivery order fails cleanly when stock runs out. An online customer has already paid, so their order is created anyway, the missing item is left undeducted, and an error alert goes out for manual follow-up.

Trade-off: a rare oversell is handled by a person, not the system.
No reservation at checkout—yet

Holding stock while a customer pays would close that gap, but only if the hold is reliably released when a payment fails or expires. The shortfall is rare and alerts, so reservation is on the backlog, not half-built.

Trade-off: the paid-but-short case stays open until it's built properly.
Test against a real database

Run tests against a real, disposable Postgres—a fresh copy of the migrated schema per test—instead of mocking the SQL. The bugs lived in SQL and timing, which a mock can't show.

Trade-off: a database in CI, and race tests that must be shown to fail before the fix.

What changed 04

Races that are proven closed, not assumed closed.

  • Eight concurrent orders for the last unit: exactly one wins, the rest get "out of stock"
  • Concurrent refunds can't take more than was paid, and a double-submitted return refund acts once
  • Concurrent cancels restock once; a cancel racing a dispatch can't both apply
  • An idempotency key replays only for the user who sent it
  • Each race has a test that hits the database at once from many goroutines—checked to fail against the old code

Lessons 05

Two customers wanting the last unit is normal traffic, not an edge case.

Make the check and the write one step, or someone gets between them.

A race test that has never failed hasn't proven anything.

When stock and money disagree, protect the money and alert a person.

An honest backlog item beats a half-built safeguard.

Building a system that has to stay consistent? 06

Let's design for
retries and race conditions.

If a workflow needs to stay correct under concurrent requests and retries—not just the happy path—I'd be glad to talk through the data model and API design.

Start a conversation ↗