The Desi Architect
Notes / Design Interview Problems / Design Flash Sale & Inventory (Blinkit / Amazon Last-Unit Race)
Track 5 · Design Interview Problems

Design Flash Sale & Inventory (Blinkit / Amazon Last-Unit Race)

Blinkit and Zepto dark stores with 10-minute delivery. When two carts race to buy the last packet of Amul butter, how to guarantee zero overselling while keeping checkout latency under 50ms.

Interview problem25 min read1 interactive
On this page
  1. Problem framing
  2. Questions I ask before drawing anything
  3. Requirements
  4. Estimation
  5. API design
  6. Database schema
  7. Concurrency strategies: from broken to bulletproof
  8. Interactive race simulator
  9. Physical reality: phantom inventory and safety buffers
  10. Edge cases and failure recovery
  11. Cross-questions interviewers will ask
  12. Interview Rubric: Mid-Level vs Senior / Staff

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)

http
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):

json
{
  "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):

json
{
  "error": "INSUFFICIENT_STOCK",
  "unavailable_items": [
    { "sku_id": "SKU_AMUL_500G", "requested": 1, "available": 0 }
  ]
}

2. Confirm Deduction (Payment Webhook Capture)

http
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):

json
{
  "status": "DEDUCTED",
  "hold_id": "hld_01HZX8921KL"
}

3. Release Hold (Explicit Cancel or Webhook Failure)

http
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.

sql
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)

ts
// 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)

sql
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:

sql
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 == 1 and proceeds.
  • When Ben runs this second: The condition available >= 1 fails. The database updates 0 rows. Ben's application sees rowCount == 0 and 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:

lua
-- 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
end
1. Checkout Tap "Pay ₹58" 2. Redis Hold Atomic Lua decrement TTL: 300s 3. Payment UPI PIN entry 4. DB Sync Permanent deduct Payment Fails or TTL Expires Lua increment: Return to available
Figure 1. Two-phase inventory reservation lifecycle. Reservation happens in Redis with a 5-minute TTL. Permanent deduction writes to Postgres only upon payment capture.

08 /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.

Try it · race simulator

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.

Shelf: Stored count: Clock:

Asha's cart

Ben's cart

Packets that exist1
Promised to customers0
Verdict-

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_inventory to decrement total_physical and available, preventing downstream orders from racing for a broken bottle.

11 /Cross-questions interviewers will ask

What if the Redis hold server crashes before the payment webhook arrives?
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.

How do you handle multi-item carts where 3 out of 4 items are available?
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?"

What you give up

Designing an atomic reservation pipeline with two-phase TTL holds prevents overselling and protects customer trust.

Here is what you sacrifice:

  1. 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.
  2. 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.
  3. 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.