Building a Zero-Oversell Multichannel Inventory Sync Engine
The problem with "just subtract the quantity"
The first version of almost every inventory system looks the same: a quantity field on a product document, decremented on every sale. It works fine until the same SKU is listed on eBay, Amazon, Shopify, and six other marketplaces simultaneously, and two orders for the last unit land within 50ms of each other. Whoever's write lands second either oversells or silently corrupts your count — and an oversold order on a marketplace is a support ticket, a refund, and a dent in your seller rating all at once.
Reservations, not decrements
The fix is to stop thinking about stock as a number you decrement and start thinking about it as a resource you reserve. When an order comes in, the sync engine doesn't subtract from available stock directly — it attempts to place a reservation against it, atomically:
// MongoDB — atomic conditional decrement
const result = await Inventory.updateOne(
{ sku, availableQty: { $gte: qty } },
{ $inc: { availableQty: -qty, reservedQty: qty } }
);
if (result.modifiedCount === 0) {
throw new OversellError(sku); // someone else got there first
}
The $gte guard inside the filter is what makes this safe under concurrency — the database itself refuses the write if the stock isn't there, rather than the application reading a stale value and deciding after the fact. No distributed lock required for the common case.
Propagating the count outward, not the order inward
Once a reservation succeeds locally, the harder problem starts: every other channel selling that SKU needs to know the available count dropped, ideally within seconds. We treat each marketplace connector as a consumer of a single internal "stock changed" event, published to RabbitMQ the instant the reservation commits. Each connector then pushes an inventory update to its marketplace API on its own retry/backoff schedule — Amazon's feed API and eBay's inventory API have wildly different rate limits and failure modes, and coupling them to the same request would mean one slow marketplace stalls stock updates everywhere else.
What actually causes oversells in practice
- Webhook replay. Marketplaces retry webhooks on timeout. If your handler isn't idempotent, a replayed "order placed" webhook double-reserves stock.
- Clock skew between "reserve" and "confirm." A reservation that's never confirmed (payment failed, buyer cancelled) has to expire and release stock automatically, or you leak inventory into a permanently reserved-but-unsold state.
- Bulk relisting jobs. A scheduled job that "refreshes" listings can race with a live sale if it reads stock before the sale's reservation commits and writes after.
The takeaway
Zero-oversell isn't a single clever trick — it's picking the one place (the database write) where you can get atomicity for free, and then building everything else (propagation, expiry, idempotency) around never trusting a read that's more than a few hundred milliseconds old.