01 /Problem framing
In quick-commerce platforms like Blinkit, Zepto, and Instamart, inventory is strictly localized to a specific neighborhood dark store. A dark store in Indiranagar, Bengaluru or Sector 56, Gurgaon holds roughly 5,000 active SKUs in a 3,000 square foot facility.
On a Sunday morning at 08:30 AM, breakfast demand surges. In the Indiranagar dark store, exactly one 500g packet of Amul butter remains on the shelf.
Asha and Ben both have that packet in their carts. Both tap "Place Order and Pay ₹58" at the exact same millisecond.
If your inventory engine has a concurrency race:
- Both payments succeed.
- Two delivery riders are dispatched to the same shelf.
- One rider gets the butter; the other rider finds an empty rack.
- Customer support must call Ben to apologize, initiate a refund, and offer a ₹50 coupon. In quick commerce, overselling destroys customer trust and unit economics.
The architectural challenge is: how do you guarantee that a finite physical item is sold to exactly one buyer under concurrent web traffic, while keeping catalog browsing fast and checkout latency under 50 milliseconds?
02 /Questions I ask before drawing anything
Reservation timing
- When do we hold inventory? Never on "Add to Cart". If adding an item to a cart held physical stock, a handful of bad actors could lock every carton of milk in the neighborhood simply by filling online carts. We reserve stock strictly when the user clicks "Proceed to Payment", with a tight 5-minute expiry window.
- What happens if payment fails? The hold must automatically expire and return the unit to the sellable pool without requiring manual customer care intervention.
Partitioning and physical realities
- How is stock partitioned? Stock is sharded strictly by
store_id + sku_id. A shopper in Indiranagar never competes with a shopper in Whitefield. - What about physical shrinkage? Real warehouses have damaged goods, dropped bottles, and misplaced packets. If the database reports 1 unit left, there is a non-trivial chance the shelf is already empty. Many platforms enforce a safety buffer of 1 or 2 units on fragile SKUs.
03 /Requirements
Functional
- Check real-time stock availability for a SKU at a specific dark store.
- Atomically reserve stock for checkout with a 5-minute Time-To-Live (TTL).
- Commit permanent deduction upon payment confirmation webhook.
- Release hold immediately if payment is rejected or explicitly cancelled by the user.
- Automated sweeper to release expired abandoned holds.
- Inward stock updates from warehouse handheld barcode scanners.
Non-functional
- Zero overselling: Never promise more stock than physically available on hand.
- Sub-50ms reservation latency: Checkout button must feel instantaneous.
- High read throughput: 100x more catalog browsing reads than checkout writes.
- High write resilience: Flash sales on mangoes or cricket World Cup beer drops must not crash the inventory database.
Below the line (Scope exclusions)
To preserve focus on high-concurrency inventory reservation and flash sale consistency during an interview, declare these items out of scope:
- Warehouse picker physical route optimization: Slicing picking tasks and calculating Traveling Salesperson paths through dark store aisles are handled by warehouse management software.
- Supply chain demand forecasting & automated re-stocking: Machine learning models predicting tomorrow's banana replenishment volumes run in offline analytics clusters.
- Supplier invoice reconciliation & returns: Generating credit notes for expired FMCG stock is part of back-office enterprise ERP systems.
04 /Estimation
| Assumption | Value | How I got it |
|---|---|---|
| Total dark stores nationwide | 1,000 dark stores | Large quick commerce footprint across top 20 Indian cities |
| Active SKUs per dark store | ~5,000 SKUs | Groceries, dairy, snacks, personal care |
| Total inventory rows | ~50 lakh (5,000,000) | 1,000 stores * 5,000 SKUs |
| Daily orders across network | 15 lakh (1.5M) | ~1,500 orders per dark store per day |
| Peak checkout orders per second | ~3,000 checkouts/sec | National peak during morning 8-10 AM or evening 7-9 PM |
| Peak checkouts per single dark store | ~25 checkouts/sec | Highly localized, meaning database write concurrency per store is modest |
| Catalog browsing read RPS | ~150,000 reads/sec | 50:1 browsing to purchase ratio |
| Memory for 5M inventory records | ~2.5 GB | 5M rows * 500 bytes (fits easily in a single Redis cluster!) |
Notice an essential architectural truth: while national volume is high (150,000 reads/sec), concurrency on a single physical item in a single dark store is bounded. Sharding by store_id distributes the load across your cluster.
05 /API design
1. Reserve Stock Hold (Proceed to Payment)
POST /v1/inventory/reserve
Host: inventory.quickcommerce.internal
Content-Type: application/json
Idempotency-Key: idemp_hold_99182a
{
"store_id": "BLR_IND_04",
"user_id": "usr_asha_42",
"ttl_seconds": 300,
"items": [
{ "sku_id": "SKU_AMUL_500G", "quantity": 1 },
{ "sku_id": "SKU_BREAD_WHEAT", "quantity": 1 }
]
}Response (201 Created - All items reserved):
{
"hold_id": "hld_01HZX8921KL",
"status": "RESERVED",
"expires_at": "2026-04-12T08:35:00Z",
"items": [
{ "sku_id": "SKU_AMUL_500G", "reserved_qty": 1 },
{ "sku_id": "SKU_BREAD_WHEAT", "reserved_qty": 1 }
]
}Response (409 Conflict - Item Out of Stock):
{
"error": "INSUFFICIENT_STOCK",
"unavailable_items": [
{ "sku_id": "SKU_AMUL_500G", "requested": 1, "available": 0 }
]
}2. Confirm Deduction (Payment Webhook Capture)
POST /v1/inventory/confirm
Host: inventory.quickcommerce.internal
Content-Type: application/json
{
"hold_id": "hld_01HZX8921KL",
"payment_reference": "pay_upi_88192a8b",
"confirmed_at": "2026-04-12T08:32:15Z"
}Response (200 OK):
{
"status": "DEDUCTED",
"hold_id": "hld_01HZX8921KL"
}3. Release Hold (Explicit Cancel or Webhook Failure)
POST /v1/inventory/release
Host: inventory.quickcommerce.internal
Content-Type: application/json
{
"hold_id": "hld_01HZX8921KL",
"reason": "PAYMENT_FAILED_INSUFFICIENT_FUNDS"
}06 /Database schema
A relational store (PostgreSQL) guarantees transactional consistency for physical inventory counts.
CREATE TABLE store_inventory (
store_id VARCHAR(32) NOT NULL,
sku_id VARCHAR(64) NOT NULL,
total_physical INT NOT NULL DEFAULT 0,
available INT NOT NULL DEFAULT 0,
reserved INT NOT NULL DEFAULT 0,
sold INT NOT NULL DEFAULT 0,
safety_buffer INT NOT NULL DEFAULT 1,
version INT NOT NULL DEFAULT 1,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (store_id, sku_id),
CONSTRAINT chk_available_nonneg CHECK (available >= 0),
CONSTRAINT chk_reserved_nonneg CHECK (reserved >= 0)
);
CREATE TABLE inventory_holds (
hold_id VARCHAR(64) PRIMARY KEY,
store_id VARCHAR(32) NOT NULL,
user_id VARCHAR(64) NOT NULL,
sku_id VARCHAR(64) NOT NULL,
quantity INT NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'HELD',
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_holds_expiry ON inventory_holds (expires_at, status);07 /Concurrency strategies: from broken to bulletproof
Let us trace the four approaches to handling the last unit race.
1. Naive Read-Modify-Write (The bug that causes overselling)
// BROKEN: Race condition between read and write
const item = await db.query(
'SELECT available FROM store_inventory WHERE store_id = $1 AND sku_id = $2',
[storeId, skuId]
);
if (item.available >= 1) {
// Concurrency window: Both Asha and Ben reach here simultaneously!
await db.query(
'UPDATE store_inventory SET available = $1 WHERE store_id = $2 AND sku_id = $3',
[item.available - 1, storeId, skuId]
);
return { status: 'RESERVED' };
}If Asha and Ben read available = 1 at the exact same millisecond, both threads pass the if check. Both write available = 0. Both are promised the packet. One packet was sold twice.
2. Pessimistic Locking (SELECT FOR UPDATE)
BEGIN;
SELECT available
FROM store_inventory
WHERE store_id = 'BLR_IND_04' AND sku_id = 'SKU_AMUL_500G'
FOR UPDATE;
-- Application checks if available >= 1, then updates:
UPDATE store_inventory
SET available = available - 1
WHERE store_id = 'BLR_IND_04' AND sku_id = 'SKU_AMUL_500G';
COMMIT;FOR UPDATE locks the database row. Ben's query blocks until Asha's transaction commits.
- The problem: Holding open database row locks while application code processes payment gateways or network calls causes connection pool starvation and severe throughput degradation.
3. Atomic Conditional Update (The relational sweet spot)
You do not need an open transaction lock. You can let the database evaluate the condition atomically in the SQL engine:
UPDATE store_inventory
SET available = available - 1,
reserved = reserved + 1,
version = version + 1
WHERE store_id = 'BLR_IND_04'
AND sku_id = 'SKU_AMUL_500G'
AND available >= 1;In PostgreSQL or MySQL, an UPDATE statement acquires an exclusive row lock only for the microsecond duration of the row modification.
- If Asha runs this first: 1 row is updated. Her application sees
rowCount == 1and proceeds. - When Ben runs this second: The condition
available >= 1fails. The database updates 0 rows. Ben's application seesrowCount == 0and immediately responds: "Sorry, this item just went out of stock".
Zero race conditions, zero deadlocks, zero dirty reads.
4. Redis Two-Phase Reservation with TTL (High scale flash sales)
During flash sales where 10,000 users click on a single discounted product, hitting PostgreSQL directly can overwhelm connection pools.
We use Redis with an atomic Lua script:
-- KEYS[1]: store:BLR_IND_04:sku:SKU_AMUL_500G
-- ARGV[1]: hold_qty (1)
-- ARGV[2]: hold_id (unique UUID)
-- ARGV[3]: hold_ttl_seconds (300)
local current = tonumber(redis.call('HGET', KEYS[1], 'available') or '0')
if current >= tonumber(ARGV[1]) then
redis.call('HINCRBY', KEYS[1], 'available', -tonumber(ARGV[1]))
redis.call('HINCRBY', KEYS[1], 'reserved', tonumber(ARGV[1]))
-- Record hold ownership and expiry
redis.call('SET', 'hold:' .. ARGV[2], ARGV[1], 'EX', ARGV[3])
return 1 -- SUCCESS
else
return 0 -- OUT OF STOCK
end08 /Interactive race simulator
Step through Asha and Ben racing for the last packet of Amul butter. Test the naive strategy to see the double-promise bug, switch to conditional updates, and test what happens when payment fails under a TTL reservation hold.
Two carts, one packet of Amul butter
A Blinkit-style dark store has exactly one packet left. Asha and Ben both press Pay. Pick a strategy, then move each cart one step at a time, or run a ready-made order. Try the unlucky order on the naive strategy first.
Stored count:
Clock:
Asha's cart
Ben's cart
09 /Physical reality: phantom inventory and safety buffers
Software architectures assume that physical inventory strictly equals the database number. In a physical warehouse or dark store, this assumption is false.
- Damaged goods: A rider picks up an egg carton and notices one cracked egg.
- Misplaced stock: A picker placed the butter on the ice cream shelf. The system thinks it exists, but the picker cannot find it within the 2-minute picking window.
- Shrinkage: Inventory theft or miscounting during supplier inwarding.
The Safety Buffer strategy
For high-velocity or perishable goods, many quick commerce teams apply a virtual floor:
- Sellable count =
max(0, physical_available - safety_buffer). - If
safety_buffer = 1, when physical count reaches 1, the app displays: "Out of Stock". - That final unit serves as insurance against damaged packaging. If an existing order has a damaged piece, the picker substitutes that buffer unit rather than canceling the customer order.
10 /Edge cases and failure recovery
1. Payment Callback Arrives after 5-Minute TTL Expiration
Asha enters her UPI PIN slowly. Her payment captures at minute 6, but the hold sweeper already expired her reservation and released the butter to Ben.
- Compensating Transaction: The payment webhook detects the hold state is
EXPIRED. It checks physical inventory; if another unit exists, it claims it. If zero units remain, it automatically issues an instant UPI reversal refund and notifies the customer: "Item sold out during payment; refund initiated".
2. Multi-Item Cart Atomic Hold Rollback
A cart contains Milk, Bread, and Butter. Milk and Bread are reserved, but Butter fails because only 0 units remain.
- The reservation service immediately rolls back the holds on Milk and Bread in the same execution cycle, preventing inventory from getting stranded across partial orders.
3. Warehouse Floor Damage During Picking
A picker accidentally drops a bottle of olive oil.
- The picker hits "Mark Damaged" on the handheld scanner. An inward deduction event updates
store_inventoryto decrementtotal_physicalandavailable, preventing downstream orders from racing for a broken bottle.
11 /Cross-questions interviewers will ask
How do you prevent lost holds if Redis loses memory?
Redis is configured with AOF (appendonly yes, fsync everysec) or master-replica failover. However, the database is always the ultimate source of truth. When the payment webhook arrives, the backend runs an idempotent conditional update against Postgres: UPDATE inventory SET reserved = reserved - 1, sold = sold + 1 WHERE .... If Redis had crashed and forgot the hold, Postgres still checks constraints before confirming.
Should you reserve items partially or abort the whole cart?
Quick commerce checkouts use an all-or-nothing atomic reservation batch. If Asha wants butter, bread, milk, and eggs, the reservation script takes all four SKUs. If eggs are out of stock, the script rolls back the holds on butter, bread, and milk, and prompts the user: "Eggs are out of stock. Would you like to proceed with the remaining items?"
Designing an atomic reservation pipeline with two-phase TTL holds prevents overselling and protects customer trust.
Here is what you sacrifice:
- Cart lockouts on abandoned sessions: If 5 users reach the payment screen and abandon their phones without paying, those 5 units sit locked for the duration of the TTL (e.g. 5 minutes). Other eager buyers will temporarily see "Out of Stock" until the sweeper unlocks them.
- Reconciliation overhead: You must maintain a reliable asynchronous sweeper job to detect and reconcile orphan holds caused by dropped network webhooks or crashed background workers.
- Database connection efficiency: Synchronous conditional updates against a relational database require careful indexing by compound key
(store_id, sku_id)to keep row-lock wait times in the sub-millisecond range.
12 /Interview Rubric: Mid-Level vs Senior / Staff
| Dimension | Mid-Level Expectation | Senior / Staff Expectation |
|---|---|---|
| Concurrency Control | Suggests reading available stock and updating with if (qty >= 1). |
Identifies read-modify-write race conditions; uses atomic SQL conditional updates (available = available - 1 WHERE available >= 1) or Redis Lua scripts. |
| Reservation Timing | Holds stock on "Add to Cart". | Proves why cart holds enable denial-of-service abuse; reserves strictly at payment checkout initiation with 5-minute TTL holds. |
| Two-Phase Commit | Couples payment processing inside an open database transaction lock. | Implements decoupled two-phase reservation: Redis TTL hold during payment -> asynchronous confirmation upon webhook capture; avoids connection pool exhaustion. |
| Failure Reconciliation | Assumes webhooks always arrive on time. | Designs automated background sweepers for expired abandoned holds; handles late payment captures with automatic compensation reversals and refunds. |
| Physical Reality | Assumes database counts perfectly match physical shelf units. | Introduces phantom inventory safety buffers (physical - buffer); designs picker damage marking and multi-item all-or-nothing cart rollbacks. |