CASE STUDY

Food and Quick-Commerce Delivery Platform (Uber Eats / DoorDash)

6 min read·1,018 words·Advanced

How to use this case study

SDE-2 / Mid

Explain the main actors (customer, merchant, courier), the order state machine, and the flow from placing an order to delivery.

SDE-3 / Senior

Go deeper on courier matching and dispatch, live location tracking, ETA estimates, and inventory accuracy for 10-minute grocery delivery.

Staff / Principal

Discuss peak dinner-time load, city-level sharding, batching orders per courier, failure handling (merchant cancels, courier drops) and marketplace balance.


0) Problem Restatement

Design a delivery marketplace like Uber Eats, DoorDash, or a 10-minute grocery app like Flipkart Minutes. Customers browse nearby restaurants or a nearby dark store (a small warehouse only for delivery), add items to a cart, and pay. Merchants accept and prepare the order. Couriers are assigned to pick it up and deliver it. Everyone tracks the order live on a map.

This has three sides that must stay in sync, plus real-time location, so it combines search, orders, payments and dispatch.

Asked at: Flipkart, Meta, Uber — 4 candidate reports between Oct 2025 and Jun 2026.

1) Requirements

1.1 Functional

  • Browse and search nearby merchants and menus (or dark-store inventory).
  • Cart, checkout and payment.
  • The merchant accepts or rejects and marks the order ready.
  • Assign a courier, then track pickup and delivery live.
  • ETAs and notifications at every step.

1.2 Non-Functional

  • Order correctness: no lost or double orders, and a correct state everywhere.
  • Low latency for browsing and for location updates (a few seconds).
  • Peak handling: dinner time is 3–5x the average.
  • Inventory accuracy for quick commerce (don't sell items that are out of stock).

1.3 Scale Estimates

  • 10M orders/day, peaking at 500 orders/sec at dinner.
  • 500K active couriers sending GPS every 4 seconds → 125K location updates/sec.
  • Browsing: 100M menu or listing views/day.

1.4 API Design

  • GET /v1/merchants?lat=&lng=&q= and GET /v1/merchants/{id}/menu
  • POST /v1/orders (Idempotency-Key) { merchant_id, items, address, payment_method }
  • POST /v1/orders/{id}/accept (merchant), POST /v1/orders/{id}/ready
  • POST /v1/couriers/{id}/location { lat, lng, ts }
  • GET /v1/orders/{id}/track (WebSocket or SSE for live updates)


2) High-Level Architecture

2.1 Overview

  • Discovery/Search: finds merchants near the address (geo index) and serves menus from a cache.
  • Cart & Order Service: builds the order, prices it, reserves inventory (quick commerce) and runs the order state machine.
  • Payment Service: authorizes at checkout and captures on delivery.
  • Dispatch Service: picks the best courier using live locations and ETAs.
  • Location Service: ingests courier GPS into an in-memory geo index and a stream.
  • Tracking/Notification Service: pushes status and courier location to the customer.

2.2 Architecture Diagram

Architecture Diagram

flowchart LR
    CU["Customer app"] --> SR["Discovery / Search"]
    CU --> OS["Order Service - state machine"]
    OS --> PAY["Payment Service"]
    OS --> INV[("Inventory - dark stores")]
    OS --> DB[("Orders DB")]
    OS --> K[("Order events - Kafka")]
    K --> DSP["Dispatch Service"]
    CO["Courier app"] -->|"GPS every 4s"| LOC["Location Service"]
    LOC --> GEO[("In-memory geo index")]
    DSP --> GEO
    DSP -->|"offer job"| CO
    ME["Merchant tablet"] --> OS
    K --> TR["Tracking + Notifications"]
    TR --> CU

3) Data Model

orders:        order_id, customer_id, merchant_id, courier_id, items (JSON), total_cents,
               status, address, created_at, eta
order_events:  order_id, status, ts, actor         -- full history
couriers:      courier_id, status (offline/available/on_job), vehicle, current_order_id
inventory:     store_id, sku, available_qty, reserved_qty   -- quick commerce
Order states: placed → accepted → preparing → ready_for_pickup → picked_up → delivered, with side paths cancelled and rejected.

4) Key Flows

4.1 Placing an order

  1. Checkout validates the cart (prices, items available, merchant open).
  2. For quick commerce, reserve stock: reserved_qty += n only if available_qty - reserved_qty >= n, in one atomic update.
  3. Authorize payment, create the order (placed) and publish an event.
  4. The merchant tablet gets the order and accepts it. If it doesn't accept within ~3 minutes, the order is cancelled and refunded automatically.

4.2 Dispatching a courier

  1. When the order is accepted, dispatch starts so the courier arrives about when the food is ready (prep time estimate − travel time).
  2. Find available couriers near the merchant from the geo index, score them (ETA to merchant, current load, ratings), and offer the job to the best one with a 30-second timeout. If declined, offer it to the next.
  3. Assignment uses a conditional update (set courier_id only if still null) so an order never gets two couriers.

4.3 Tracking

Courier GPS → Location Service → a stream per active order → pushed to the customer's app every few seconds with an updated ETA.


5) Deep Dive A — Dispatch and batching

  • Greedy vs batch matching: instead of assigning each order the moment it's ready, collect orders and couriers every ~10–30 seconds in each area and solve a small assignment problem that minimizes total delay. That's better overall than first-come-first-served.
  • Stacking: one courier can carry 2 orders from the same restaurant or nearby ones if it doesn't delay the first order too much.
  • City sharding: dispatch works per city or zone, since orders never cross cities. This splits the load naturally.


6) Deep Dive B — Quick commerce inventory and failures

  • Stock is counted per dark store. Reservations expire if payment fails (e.g., after 10 minutes), which releases stock.
  • Pickers scan items when packing. If something is missing, the customer gets an instant substitution or partial refund.
  • Failures: the merchant rejects → refund. The courier cancels → re-dispatch at higher priority. The app crashes mid-checkout → the idempotency key prevents double orders. Every state change is an event, so any service can rebuild what happened.


7) Trade-offs & Alternatives

DecisionChoiceWhyAlternative
Order consistencyStrongly consistent order DB + state machineMoney and fulfillment must be exactEventual NoSQL: risk of double assignment
Courier locationsIn-memory geo index from a streamUpdates every 4s, instant queriesWrite every GPS point to a DB: too slow and costly
MatchingBatched assignment per zoneBetter global efficiencyGreedy nearest: simple, worse at peaks
IntegrationEvents via KafkaLoose coupling, replayableDirect calls everywhere: fragile chains

8) Common Follow-up Questions

  • "How do you compute ETAs?" An ML model trained on past trips (distance, traffic, time of day, restaurant prep history), updated live during delivery.
  • "Dinner peak?" Autoscale stateless services, pre-scale before known peaks, and use surge incentives to bring more couriers online.
  • "How do you cache menus?" Menus change rarely. Cache them per merchant with invalidation on edit, and serve them via CDN.


9) Wrap-Up

Use a strict order state machine in a consistent DB, emit events for every change, and let separate services handle payment, dispatch, tracking and notifications. Keep courier locations in an in-memory geo index fed by a stream, match couriers in small batches per zone with a conditional assignment, and for quick commerce reserve inventory atomically with expiring holds.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →