Design problems
E-commerce & marketplacesHardSDE-2 35 min

Design 10-minute grocery delivery (Zepto / Blinkit)

Asked at
AmazonBlinkitFlipkartSwiggyZepto

The interviewer asks

“Design a quick-commerce app that delivers groceries in about 10 minutes from dark stores. Cover how you show only what is actually in stock nearby, how orders are picked and assigned to riders, and how you keep the 10-minute promise honest.”

What a strong answer looks like

A strong answer explains that 10 minutes is possible only because the problem is local: each user is served by exactly one dark store within 2-3 km, so the catalog, inventory and riders are per store. Start with serviceability (map the user's location to a store polygon), then a per-store catalog that shows real-time stock, then checkout that reserves stock atomically in that store, then an orchestration of pick-pack (about 2-3 minutes) and rider assignment (riders wait at the store). The ETA shown before checkout must come from live signals: picker queue, rider availability and travel time, and the system must stop taking orders or widen the promise when the store is overloaded. Deep dives: inventory accuracy and reservations, ETA and load shedding per store, and rider dispatch. Mention festive and rain spikes, and the need to keep the store running if the cloud link is slow.

Concepts to mention

location to dark store mapping (polygon / H3)per-store catalog and inventoryatomic multi-item reservation with TTLavailable = on_hand - reservedETA from picker queue, rider availability and travel timeper-store admission control / surgerider assignment timed to pick progressoutbox + Kafka for order eventsinventory reconciliation and cycle countsidempotent offline-tolerant store apps

1 Requirements

Functional

  • Detect the user's serviceable dark store from their location and show the catalog of that store only.
  • Show live stock; hide or mark items that are out of stock in that store.
  • Cart and checkout with UPI, cards, wallet or COD; apply offers and delivery fee.
  • Reserve stock at checkout and release it on payment failure or timeout.
  • Send the order to the store's picker app; pickers pick and pack with item-level scanning.
  • Assign a rider, track the rider live, and notify the customer.
  • Show an honest ETA before checkout and handle substitutions or missing items with refunds.

Non-functional

  • Median order-to-door under 10-12 minutes; pick-pack under 3 minutes.
  • Catalog page loads in under 1 s; stock shown is accurate within a few seconds.
  • Overselling rate under 0.5% of orders (item missing after payment).
  • Checkout p99 under 800 ms excluding the payment provider.
  • 99.95% availability; one store or region failing must not affect others.
  • Handle 5-8x spikes during rain, cricket matches and festivals (Diwali, New Year's Eve).

Out of scope (say it out loud)

  • Warehouse-to-dark-store replenishment planning (mention only).
  • Vendor procurement and pricing systems.
  • Rider payroll.

2 Estimates

Dark stores~1,000 storesLarge players run several hundred to over a thousand stores across ~30-50 cities.
Orders~1.5M/day, peak ~60/s, ~2-3 orders/min per busy store1.5M ÷ 86,400 ≈ 17/s average; evening peak ×3.5 ≈ 60/s. 1.5M ÷ 1,000 stores ≈ 1,500/day per store; busy hours ~150/hour ≈ 2.5/min.
SKUs per store~6,000-10,000A 3,000-4,000 sq ft dark store carries a few thousand fast-moving SKUs.
Inventory rows~10M (store × SKU)1,000 stores × 10,000 SKUs. Fits easily in Redis and a sharded database.
Catalog reads~30k/s peak~5M sessions/day × ~50 product/listing views ≈ 250M/day ≈ 3k/s; peak ×10 at dinner time.
Rider location updates~5k/s~20k active riders × 1 update every 4 s.
Picker capacity~20-25 orders/hour per pickerPick + pack ≈ 2.5-3 minutes for a 6-item order.

3 API

EndpointWhat it does
GET/v1/serviceability?lat=&lng=Return store_id, delivery ETA band and fee, or 'not serviceable / store busy'.
GET/v1/stores/{store_id}/catalog?category=Catalog for that store with price and in-stock flags.
POST/v1/carts/{cart_id}/checkoutValidate cart, reserve stock, compute ETA and total, return payment intent.
POST/v1/ordersConfirm order after payment success; Idempotency-Key required.
WS/v1/orders/{id}/trackLive status: packing, rider assigned, rider location, arriving.
POST/v1/store/{store_id}/picks/{order_id}/itemPicker app scans an item (barcode) or marks it missing.
POST/v1/riders/{id}/locationRider app sends location batches every few seconds.

4 Data model

store_polygonsPostgres + PostGIS (cached in memory)
store_idpolygoncitystatus (open|busy|closed)max_eta

Point-in-polygon lookup to find the store. Small table; each serviceability node loads it into memory with an R-tree or H3 cells.

inventoryPostgres (sharded by store_id) + Redis
store_idsku_idon_handreservedversion

Available = on_hand - reserved. Redis holds the hot counters for fast reservation; Postgres is the durable record. Partition by store_id so a store's data lives together.

reservationsRedis (TTL) + Postgres
reservation_idcart_idstore_iditems [{skuqty}]expires_at

Expire after ~5 minutes if payment does not succeed; a sweeper releases stock.

ordersPostgres (sharded by store_id)
order_iduser_idstore_iditemsamountsstatuspromised_etapicker_idrider_idtimestamps per stage

Stage timestamps power ETA models and SLA dashboards.

store_loadRedis
store_id → pending_picksactive_pickersidle_ridersavg_pick_secs

Updated in real time from events; read by ETA and admission control.

rider_locationsRedis (GEO) + Kafka
rider_idlatlngtsstate

Latest position in Redis; full trail to Kafka for analytics and disputes.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALlocationwhich store, ETAcheckoutstock flagsreserve stockOrderConfirmedpick listready in ~2 minidle riderspicked / restockedstore loadCustomer appRider appPicker app (store)API gatewayServiceability + ETAStore catalog serviceCart + order serviceFulfilmentorchestratorInventory sync workerRider dispatch workerKafka (order, stockevents)Redis (stock, load,GEO)Postgres (orders,stock)Maps / routing APIPayment gateway
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Open the app and see the store

  1. 1App sends the user's location to /serviceability.
  2. 2Serviceability service finds the store polygon containing the point (in-memory H3 or R-tree lookup, under 5 ms).
  3. 3It reads store_load from Redis and computes an ETA band, e.g. '9 min'. If the store is overloaded it returns 'busy' with a longer ETA or a surge fee.
  4. 4Catalog service returns that store's catalog; in-stock flags come from Redis available counts, and the catalog itself is cached per store at the CDN for a few seconds.

Checkout and pick-pack

  1. 1User taps Pay; order service runs a Redis Lua script that checks and decrements available for every item in the cart atomically, creating a reservation with a 5-minute TTL.
  2. 2If any item fails, the script reserves nothing and returns the missing items so the app can update the cart.
  3. 3Payment is done through the gateway; on success the order is written to Postgres with the reservation and an OrderConfirmed event goes out through the outbox.
  4. 4Fulfilment orchestrator picks the least-loaded picker and pushes the pick list sorted by shelf path to the picker app.
  5. 5Picker scans each item; a missing item triggers a substitution suggestion or a partial refund, and an inventory correction event.
  6. 6When the pick is about 60% done, dispatch assigns a rider so the rider is at the counter when the bag is sealed.

Rider delivery

  1. 1Dispatch worker chooses an idle rider at the store (or returning within 2 minutes) and can batch two orders going the same direction if both still meet their ETA.
  2. 2Rider picks up, scans the bag and starts; location updates every 4 s go to Redis and the tracking WebSocket.
  3. 3On delivery the rider marks delivered (OTP for high-value orders); stage timestamps feed the ETA model.

7 Deep dives

Inventory accuracy and reservation

Overselling is the worst failure in quick commerce: the user paid, and the picker finds an empty shelf. The fix has two parts. First, accurate counts: every pick, receipt, damage and cycle-count is an event that updates on_hand in the store's partition; a store team does daily cycle counts of fast movers and the system flags SKUs whose sales stopped suddenly (often a phantom stock sign). Second, safe reservation: at checkout, run one Redis Lua script per store that checks available = on_hand - reserved for all cart items and increments reserved only if all pass. Because a store's keys live on one Redis shard (hash tag {store_id}), the script is atomic and fast, a few hundred microseconds. The reservation has a TTL; a sweeper releases expired ones, and payment success converts it to a committed decrement written to Postgres. Redis is not the source of truth: a sync worker rebuilds Redis counts from Postgres on startup and checks drift every few minutes. Also keep a small safety buffer (e.g. show out of stock when available is 1 for items with frequent count errors), trading a little lost sales for far fewer missing-item refunds.

Honest ETA and per-store admission control

The 10-minute promise is three parts: wait for a picker, pick and pack (~2-3 min), and ride (~4-6 min for 2 km in a city). The ETA service computes each part from live signals in Redis: pending picks divided by active pickers times average pick time, the number of idle riders and riders returning, and travel time from a routing service with time-of-day traffic. A gradient-boosted model trained on stage timestamps corrects bias by store and hour. When the predicted ETA crosses a threshold, the store must protect its promise instead of breaking it for everyone: raise the delivery fee (surge), show a longer ETA, shrink the store polygon temporarily, or mark the store 'busy' and stop accepting orders for a few minutes. This is load shedding at the business level, and it must happen per store, because one store in heavy rain can be overloaded while its neighbour is idle. Track promise accuracy (percentage delivered within shown ETA) as the key SLO.

Rider dispatch at the store

Unlike food delivery, riders here start from the same place: the dark store. So dispatch is mostly a queue problem, not a matching problem across a city. Riders who finish a delivery head back and join the store's idle queue. The dispatcher assigns when the pick is about 60% complete, picking the rider who will be at the store soonest, so neither the bag nor the rider waits. Batching two orders is allowed only when both drop points are in the same direction and the second customer's promised ETA still holds; the dispatcher simulates both routes before batching. During spikes, the system can borrow riders from a nearby store whose load is low, at the cost of a longer first leg. Rider location updates (~5k/s) go to Redis GEO for the latest point and to Kafka for history; the WebSocket gateway pushes updates to customers watching the order.

Store resilience when the network is bad

The dark store is a physical operation that cannot stop because of a cloud blip. Picker and rider apps keep a local queue of actions (scans, item missing, handover) and retry with idempotency keys, so a brief outage does not lose events. A small edge service or a cached pick list on the device lets pickers continue the orders they already have. The order service rejects new orders for a store whose heartbeat is stale, so we never accept orders a store cannot see. Each store is an independent unit in the data model (partitioned by store_id), which also lets you roll out changes store by store.

8 Trade-offs

Redis reservations vs database row locks

Row locks with SELECT ... FOR UPDATE in Postgres are simple and durable but slow under hot items like milk and bread at 8 am. Redis Lua scripts give atomic multi-item reservation in microseconds; the cost is a second copy that can drift, so you need reconciliation from Postgres. At a few orders per minute per store either works, but sale and rain spikes favour Redis.

Show out of stock early vs risk overselling

A safety buffer hides items with one or two units left. You lose some sales, but a missing item after payment costs a refund, a bad rating and picker time. Tune the buffer per SKU from its count-error history.

Assign rider early vs late

Assigning at order time makes riders wait idle at the counter; assigning after packing makes bags wait. Assigning at around 60% of the pick, based on predicted remaining pick time, balances both.

One store per user vs multi-store carts

Serving each location from exactly one store keeps inventory and dispatch simple and fast. The cost is a smaller assortment; some apps add a slower 'more items' option from a larger warehouse as a separate order.

9 Bottlenecks

Rain or match-night spike

Orders jump 5-8x within minutes while riders slow down. Admission control per store (surge fee, longer ETA, temporary polygon shrink) is the main defence; autoscale stateless services on request rate and keep Redis well under its CPU limit.

Hot SKUs at a single store

Thousands of reservations for a few SKUs in one store hit one Redis shard. Still fine at a few hundred ops/s; at 10x, keep reservations per store small and fast, and move non-critical reads (catalog flags) to replicas.

Catalog cache churn

Stock flags change every few seconds, so caching whole pages is hard. Cache the static catalog per store at the CDN and fetch stock flags as a small separate call.

10 Failure handling

Payment succeeds but order write fails

The reservation is still held with TTL. A payment webhook consumer creates the order idempotently from the payment intent; if stock was released by then, it refunds automatically. Reconciliation with the gateway's settlement report catches any gap.

Redis node holding a store's counters fails

Failover to the replica; counts may be a few writes behind. Pause checkout for affected stores for a few seconds, rebuild counts from Postgres (on_hand minus active reservations), then resume.

Picker finds an item missing

The picker app marks it missing; the system offers a substitute or refunds that item immediately, decrements on_hand to zero and triggers a cycle count for that bin.

11 What impresses

  • Breaks the 10 minutes into picker wait, pick-pack and ride, and estimates each.
  • Treats overselling as the key failure and designs reservation plus count accuracy, not just a counter.
  • Proposes per-store load shedding (surge, longer ETA, busy flag) to protect the promise during rain.
  • Notices that dispatch is a store-local queue problem, unlike city-wide food delivery matching.
  • Partitions everything by store_id so stores are independent failure and rollout units.
  • Measures promise accuracy as the SLO, not just average delivery time.

12 Mistakes

  • Designing a central warehouse e-commerce system with global inventory.
  • Showing a fixed '10 minutes' that ignores store load.
  • Decrementing stock only after payment, so two users buy the last unit.
  • Reserving items one by one without atomicity, leaving partial reservations.
  • Using Redis as the only source of truth for inventory with no reconciliation.
  • Ignoring the picker step and jumping straight to rider matching.

13 Follow-ups

Q1How do you decide store polygons and when to resize them?
Q2How would you forecast demand per store per hour for replenishment?
Q3A user is on the boundary of two stores. Which one serves them?
Q4How do you handle a cart where one item goes out of stock during payment?
Q5How would you add a 'scheduled delivery' slot option?
Q6What metrics would you put on a store operations dashboard?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free