Superficially food delivery, but the ten-minute promise changes the model completely. There is no restaurant preparing to order — there are
dark stores holding physical stock, and the design problem becomes inventory
concurrency and dispatch batching.
What makes it different
| Food delivery | Quick commerce | |
|---|---|---|
| Supply | Cooked on demand | Finite physical stock |
| Catalogue | Per restaurant | Per dark store, varies by location |
| Availability | Restaurant open/closed | Per-SKU stock count |
| Time budget | 30–45 min | 10 min, end to end |
| Failure mode | Late food | Out of stock mid-order |
That third row is the crux: a restaurant can always cook another dosa, but there are exactly seven bottles of milk in the store. **Overselling is the failure this design exists to prevent.**
The domain model
DarkStore id, location, serviceable geofence, operating hours, staff
Product sku, name, category, unit, image
Inventory (store_id, sku) → available, reserved, updated_at
Cart customer, store_id, items
Order id, customer, store, items, status, slot, timestamps
Picker id, store, current picklist
Rider id, current location, status, assigned tripsInventory is keyed by (store, SKU), not by product alone. The same SKU has different
stock in every store, and a customer only ever sees one store's catalogue.
Serviceability
Before anything else, resolve the customer's address to a store:
interface ServiceabilityResolver {
Optional<DarkStore> resolve(Location customerLocation);
}Each store has a polygon (or a set of geohash cells) it serves, sized by what a rider can reach in the delivery window. If the address falls in no polygon, the app must say so
before the customer builds a cart — the most common product complaint here is discovering
non-serviceability at checkout.
Overlapping coverage means choosing: nearest, or the one with better stock for this cart.
Inventory and reservations
The heart of the design. Three quantities, not one:
on_hand physically in the store
reserved committed to carts/orders not yet picked
available = on_hand - reserved ← what customers can addReserving at checkout, not at add-to-cart, is the right default: reserving on add would let an abandoned cart block stock for hours. A short reservation TTL covers the checkout window, as with a hotel booking hold.
boolean reserve(String storeId, String sku, int qty) {
// The predicate IS the concurrency control — no read-then-write race.
int updated = jdbc.update("""
UPDATE inventory
SET reserved = reserved + ?
WHERE store_id = ? AND sku = ?
AND on_hand - reserved >= ?
""", qty, storeId, sku, qty);
return updated == 1; // 0 rows ⇒ insufficient stock
}The conditional update makes overselling impossible regardless of how requests interleave —
the same optimistic technique as hotel inventory. Back it with a
CHECK (reserved <= on_hand) constraint.
A multi-item cart must reserve all or nothing in one transaction, with items locked in a consistent order (by SKU) to avoid deadlocks between two overlapping carts.
The order pipeline
PLACED → PICKING → PACKED → DISPATCHED → DELIVERED
│ │
└────────┴──→ CANCELLED (release reservations)Every state has a time budget, because the promise is ten minutes end to end:
pick + pack: ~2 min
dispatch: ~1 min
ride: ~5-7 minTrack elapsed time per stage and escalate when a stage overruns — the SLA is per stage, not just overall, because by the time the total is blown it is too late to recover.
Picking and batching
class PickList {
List<PickItem> items; // sorted by AISLE ORDER, not by cart order
}Sorting the pick list by physical aisle position rather than the order the customer added items is a small change that measurably shortens picking. It is the kind of domain detail that lifts an answer.
Dispatch batches orders heading the same way:
interface BatchingStrategy {
List<Trip> batch(List<Order> ready, List<Rider> available);
}Batching two or three nearby orders into one trip is the main lever on delivery cost — and it directly threatens the SLA, since the second drop waits for the first. Cap batch size and the detour distance, and never batch an order that is already close to its deadline. Stating that tension is the point.
Out of stock at picking
The failure mode unique to this domain: the reservation said seven bottles, the shelf has five. Options, and the design should support all three:
- Substitute with a similar SKU (requires a substitution graph and customer consent).
- Partially fulfil and refund the difference.
- Cancel the item, notify immediately.
Whichever happens, on_hand must be corrected — this is the moment the system learns its
inventory was wrong. Cycle counts and reconciliation against actual shelf stock is the
background process that keeps the numbers honest, and it is worth naming.
Patterns worth naming
- Strategy — serviceability resolution, batching, substitution.
- State — the order pipeline.
- Observer — stage transitions driving customer notifications.
- Repository — inventory access, so the conditional update is in one place.
- Command — reservation and release as reversible operations.
Edge cases to handle
- Store goes offline mid-order (power cut, staff shortage) → reassign or cancel.
- Reservation TTL expires while payment is processing.
- Rider cancels after pickup.
- Address edited after ordering, to a different store's zone.
- Surge demand — throttle new orders rather than accepting orders you cannot deliver.
Extensions to be ready for
- Demand forecasting to decide what each store stocks; the real competitive advantage.
- Slot-based delivery for non-instant orders.
- Dynamic delivery fees by distance and demand.
- Multi-store fulfilment for a cart no single store can satisfy.
- Returns and refunds, and returning stock to
on_hand.
Track this problem on the System Design sheet.
Frequently asked
How does quick commerce differ from food delivery in design terms?
Food is cooked to order, so supply is effectively unbounded and the design centres on the order state machine. Quick commerce sells finite physical stock from dark stores, so the design centres on per-store, per-SKU inventory with reservations and on a ten-minute time budget split across picking, dispatch and riding. The characteristic failure is overselling, not lateness.
How do you prevent overselling inventory?
Track on-hand and reserved quantities separately and make the reservation a conditional update: increment reserved only where on_hand minus reserved is at least the requested quantity. Zero rows affected means insufficient stock. The predicate is the concurrency control, so no interleaving can oversell, and a CHECK constraint backs it up.
When should inventory be reserved — at add-to-cart or at checkout?
At checkout, with a short TTL. Reserving at add-to-cart would let abandoned carts block scarce stock for hours, which in a store holding a few units per SKU is unacceptable. The TTL covers the checkout window and releases automatically, exactly like a hotel booking hold.