I built an online store for a shop in Towanda, Pennsylvania. Almost everything they sell is one of a kind. That single fact breaks the assumption most ecommerce code is built on, and fixing it honestly meant admitting there was one failure I could not prevent.
The client is Next Game Performance, a small retailer selling used and one-off PC and gaming hardware. A graphics card comes in, it gets photographed and listed, and there is exactly one of it. When it sells, it’s gone. Quantity one.
Most store software assumes quantity is large and slow-moving. You have forty of a shirt. Two people buying at the same second is not a problem, because there were forty and now there are thirty-eight. The math forgives you. On a one-of-a-kind inventory the math does not forgive you. There is one. If two people buy it in the same few seconds, you have taken two payments for an item you can ship to one person.
I want to walk through how I handled that, because the interesting part is not the lock I wrote. The interesting part is the failure I decided I could not design away, and what I did instead of pretending I could.
01The problem that has no clean solution
Picture two customers landing on the same used RTX 4070 within a few milliseconds of each other. Both click add to cart. Both go to pay. Only one of them can actually receive the card.
The naive fix is to decrement stock the moment someone adds to cart. That breaks immediately, because people add things and abandon them constantly. You would show the item as sold the instant one person glanced at it, and it would stay sold until some timer rescued it. The other naive fix is to check stock at the final step, right before charging. That narrows the window but does not close it, and it closes it in the worst possible place: after the customer has already entered their card.
Here is the part that took me a while to accept. There is a real gap between the moment a customer reserves an item and the moment their payment actually clears. During that gap the customer is over on Stripe’s servers, typing a card number, maybe getting a 3D Secure challenge from their bank. My system does not control that time. It cannot. The reserve-and-pay sequence is distributed across two systems and a human being, and distributed systems have windows you cannot collapse to zero.
So I stopped trying to make a collision impossible. That was the turn. I started trying to make it rare, and to make sure that on the rare occasion it happened, it announced itself loudly instead of corrupting data in silence.
02Reservations, not decrements
The source of truth for stock is one number on the product: quantity. It only changes when something is genuinely paid for or cancelled. It is not touched by carts.
Carts hold reservations instead. When you add an item, the system writes a short-lived lock that says this cart has reserved this quantity until this time. Default window is fifteen minutes. The product page reads stock as the true quantity minus everything currently locked by other carts. If that comes out to zero, the page says reserved, check back in fifteen minutes, rather than lying about it being available.
The race lives inside the act of taking a reservation. Two carts checking availability at the same instant could both read available and both reserve. To stop that, every reservation attempt on a given product is serialized through a Postgres advisory lock:
export async function tryLock(productId, cartId, qty) {
return db.$transaction(async (tx) => {
// serialize every reservation on this product,
// across every running server instance
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${productId}))`;
const product = await tx.product.findUnique({
where: { id: productId },
select: { quantity: true },
});
const others = await tx.inventoryLock.aggregate({
where: { productId, cartId: { not: cartId }, expiresAt: { gt: new Date() } },
_sum: { qty: true },
});
const available = product.quantity - (others._sum.qty ?? 0);
if (qty > available) return { ok: false, available };
// reserve it for this cart, fifteen-minute expiry
await tx.inventoryLock.upsert({ /* ... */ });
return { ok: true };
});
}pg_advisory_xact_lock(hashtext(productId)) is the whole trick. It takes a lock keyed to the product, held inside the database itself, so it serializes reservation attempts on that one product even if the requests are hitting different server processes. It releases automatically when the transaction ends, so a crash can’t strand it. And because the code never holds more than one product lock at a time, there are no deadlocks to reason about.
The lock window stretches to thirty minutes when a customer actually starts a Stripe checkout, because the payment leg takes longer than browsing. But it never extends past thirty minutes once payment is in flight, which prevents a hung Stripe session from creating a zombie reservation that locks an item forever.
— The whole design I stopped trying to makea collision impossible.I made it rare,and I made it loud.
03The payment side takes the same lock
Reservations narrow the window. They do not close it, because of that distributed gap I described. The closing move happens on the payment webhook, the message Stripe sends my server when a charge actually succeeds.
That handler is careful in two ways before it touches stock. It records every Stripe event ID in a table with a unique constraint, so a webhook delivered twice (Stripe retries for three days) is caught as a duplicate and ignored. It also checks the order’s own status, so an order already marked paid is never processed a second time. Idempotency from both directions.
Then, inside a single transaction, it takes the same advisory lock on each product before decrementing. Same key, same serialization. The reservation side and the payment side queue up behind the exact same gate.
04Oversold is a signal, not a crash
Now the honest part. Despite all of that, there is a sequence where two people pay for the last unit. Customer B’s reservation expires, customer A reserves and pays, customer B’s already-open Stripe session also completes. Both charges clear. The window was small. It was not zero.
When the webhook does the math and finds it is about to drive stock below zero, it does not throw an error and it does not silently let the number go negative. It clamps the quantity to zero, and it writes a record:
const newQty = product.quantity - item.qty;
if (newQty < 0) {
oversold.push(product.slug);
await tx.product.update({ where: { id }, data: { quantity: 0 } });
} else {
await tx.product.update({ where: { id }, data: { quantity: newQty } });
}That oversold list turns into an OrderEvent of type oversold, attached to the order, plus an alert to the shop owner. The order is still marked paid, because the customer’s money is real and the system must not pretend otherwise. But instead of the normal confirmation, that customer sees payment received, we’ll be in touch. The owner gets told immediately, picks one customer to fulfill and one to refund with an apology, and the event is sitting in the order history as a permanent record of exactly what happened.
The system tracks how often this fires. If it ever happens more than about once a month, that is the signal to tighten the reservation window. Until then, the cost of the rare collision is one apologetic email and one refund, which is a cost a small shop can absorb. The cost of a silent oversell is a customer who paid, waited, and got nothing, with no record of why. That cost is trust, and a shop this size cannot absorb that one.
05What this is actually about
The lock is maybe forty lines of code. Anyone can write a lock. The part that took judgment was deciding what to do about the failure the lock cannot reach.
There were two easy roads and I took neither. The first easy road is to ignore the edge case, ship the optimistic check, and let the rare oversell happen silently. Most small-store builds do this without ever noticing the gap exists, and it works fine right up until the day it doesn’t, and then it fails as a furious customer instead of a log entry. The second easy road is to over-engineer: a global queue, a single-threaded checkout, heavyweight distributed coordination, all to drive the probability to zero. That buys correctness by killing throughput and adding operational weight a one-person shop will never maintain.
The senior move is the middle one. Bound the probability with a cheap, correct mechanism. Then take the residual failure you could not eliminate and make it loud, recorded, and recoverable. Rare by design. Never silently broken.
That principle is not really about ecommerce. It shows up anywhere a system meets the real world, where some failures genuinely cannot be designed out. The discipline is refusing to let those failures hide. You make them rare, you make them visible, and you make sure a human can recover the situation when they fire. A silent failure is a debt that comes due at the worst possible time, charged to the person who trusted you. A loud one is just a task in someone’s inbox.
This is one shop in one small town. But it is a real production system, taking real payments, built end to end and handed to an owner who is not a programmer and should never have to think about advisory locks. That is the work I care about. Not the demo that looks right on stage. The system that stays honest when two strangers click buy at the same second and only one of them can win.
Drafted with Bishop, my AI partner.
Words picked, edited, and approved by me.