operations
One Stock Number, Six Sales Channels: How Multi-Channel Inventory Actually Breaks
Selling on web + Telegram + ChatGPT + WhatsApp + voice with one inventory pool sounds simple. The way it goes wrong is always the same.
The dream is: one product table, six sales surfaces, and the stock count just works. The real problem is the unit of consistency, not the surfaces.
The race condition every multi-channel store hits
Two customers, two channels, last unit in stock.
- 00:00.000 — Customer A on the website opens the PDP. Server reads
stock = 1. - 00:00.040 — Customer B in Telegram asks the bot, "is this still available?". Bot reads
stock = 1. Replies "yes." - 00:00.180 — Customer A clicks Buy.
- 00:00.220 — Customer B says "I'll take it." Bot calls
add_to_cart. - 00:00.310 — Customer A's checkout writes
stock -= 1(now 0). - 00:00.350 — Customer B's checkout writes
stock -= 1(now -1).
You have an oversold inventory and one of them is going to get an apology email.
What works: pessimistic write at the SKU row
Every cart-add or checkout that wants to consume stock takes a row-level lock on the SKU row, reads the current count inside the lock, decides, writes, releases. The website blocks for 50ms while the Telegram bot finishes its decrement; the second one sees the truth and bails. Both customers get the right answer.
In Prisma:
await prisma.$transaction(async (tx) => {
const product = await tx.product.findUnique({ where: { id }, select: { stock: true, trackStock: true } });
if (product?.trackStock && product.stock < qty) throw new Error("OUT_OF_STOCK");
await tx.product.update({ where: { id }, data: { stock: { decrement: qty } } });
});
SQLite serialises by file lock; Postgres serialises by row lock; either way the second transaction blocks until the first commits.
What doesn't work: caching
Read replicas, Redis caches, denormalised stock-counts-per-channel — all of these win latency on the read side and lose correctness on the write side. The inventory write must be authoritative; read paths can be cached, but the cache must be invalidated synchronously on every write or it lies to the next channel that asks.
What goes wrong with reservations
Many platforms try to "reserve" stock when the cart starts and "commit" at checkout. This works until the cart is abandoned and nobody clears the reservation. After three months, your authoritative stock and your displayed stock have drifted by 40%. The fix is a reservation TTL — reserved stock auto-releases after N minutes of cart inactivity.
What the AI channels add
Each AI channel — ChatGPT, Telegram, voice — is a tool consumer talking to your store. The race-condition surface multiplies, but the fix doesn't change. Tool calls hit the same atomic decrement. The AI doesn't know it's in a race; the database does.
The merchants who never have an oversold incident treat inventory as a bank account: every withdrawal locks first, reads inside the lock, decides, writes. Boring, fast, correct.