Skip to content
System Design

Learn

Design a Quick Commerce App (LLD)

Low level design for 10-minute delivery: dark store inventory with reservations, serviceability by geofence, batched picking and dispatch, and the concurrency control on stock.

4 min readUpdated 2 Sept 2026

#LLD#Inventory#Concurrency#Logistics

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 deliveryQuick commerce
SupplyCooked on demandFinite physical stock
CataloguePer restaurantPer dark store, varies by location
AvailabilityRestaurant open/closedPer-SKU stock count
Time budget30–45 min10 min, end to end
Failure modeLate foodOut 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 trips

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

java
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 add

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

java
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 min

Track 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

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

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

  1. Substitute with a similar SKU (requires a substitution graph and customer consent).
  2. Partially fulfil and refund the difference.
  3. 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.

Related