Courses › System Design › Question Breakdowns

Design DoorDash (Food Delivery)

An order state machine for three parties, timed dispatch and batch matching, courier locks, live tracking and ETAs.

Design a food delivery platform like DoorDash: customers order from nearby restaurants, restaurants prepare the food, and a courier (Dasher) is assigned to pick it up and deliver it, with live tracking throughout. It's a three-sided marketplace question about order state machines, dispatch (assigning couriers to orders), live location and accurate ETAs.

Requirements

Functional

  • Customers can browse nearby restaurants and menus, and place an order
  • Restaurants accept orders and mark them ready
  • The system assigns a courier to each order; couriers accept, pick up and deliver
  • Customers see order status and the courier's live location, with an ETA

Out of scope: payments internals (use a payment service), ratings, promotions, grocery substitutions.

Non-functional

  • Scale: ~1M couriers online at peak, ~20M orders/day (dinner peaks ~1k orders/s)
  • Dispatch quality: minimize delivery time and courier idle time; food shouldn't sit waiting
  • Correctness: an order is assigned to exactly one courier; orders are never lost
  • Low latency for tracking updates (a few seconds)
  • Highly available — a down platform means cold food and lost restaurant revenue

[!TIP] Three parties, one order Customers, restaurants and couriers each see and change the same order. Make the order state machine the backbone, and let every service react to its transitions. Dispatch is then "a service that watches for orders needing a courier".

Capacity estimates

Core Entities

  • Restaurant — location, hours, prep-time stats; Menu items
  • Order — customer, restaurant, items, status, assigned courier, timestamps per stage
  • Courier — status (offline / available / on delivery), current location, vehicle
  • Assignment / offer — order, courier, offered at, accepted/declined

API

GET  /v1/restaurants?lat=..&lng=..&cuisine=thai
POST /v1/orders   Idempotency-Key: ...   { "restaurantId": "r_5", "items": [...], "address": {...} }
→ 201 { "orderId": "o_1", "status": "placed", "eta": "19:42" }

POST /v1/restaurant/orders/{id}/accept   { "prepMinutes": 15 }
POST /v1/couriers/location               { lat, lng, t }
POST /v1/couriers/offers/{offerId}/accept
GET  /v1/orders/{id}/track   (WebSocket/SSE: status + courier location)

High-Level Design

cust = client "Customer App" @0,0
order = service "Order Service" @1,0
odb = store "Orders DB" @2,0
rest = client "Restaurant Tablet" @0,1
events = queue "Order Events" @2,1
disp = service "Dispatch Service" @3,1
loc = cache "Courier Locations\n(geo index)" @3,2
cour = client "Courier App" @4,2
eta = service "ETA Service" @3,0
track = service "Tracking\n(WebSocket)" @1,2
cust -> order : place order
order -> odb
rest -> order : accept / ready
odb --> events
events -> disp
disp -> loc : nearby couriers
disp -> eta
disp -> cour : offer
cour -> loc : ping every 4 s
loc -> track
track -> cust : live location
  • Place: the order service validates and stores the order (placed) and charges via the payment service.
  • Restaurant: accepts with a prep time → confirmed.
  • Dispatch: reacting to order events, finds candidate couriers near the restaurant, scores them with the ETA service, and offers the order to one (or batches orders).
  • Courier: accepts → assigned; picks up → picked_up; delivers → delivered.
  • Tracking: courier pings flow to customers over a WebSocket/SSE channel.

Deep Dives

1. The order state machine

placed -> confirmed -> assigned -> picked_up -> delivered
placed -> cancelled
confirmed -> cancelled

Every transition is a conditional update (WHERE status = 'confirmed') and emits an event. Each actor can only perform its own transitions, and retries are safe. The event stream drives dispatch, notifications, ETAs and analytics.

2. Dispatch: when and to whom

Naively offering the order to the nearest courier as soon as it's placed fails two ways: the courier arrives long before the food is ready (idle courier), or a slightly farther courier finishing a nearby drop would have been better.

  • Time the dispatch: start looking so the courier arrives when the food is ready — order time + predicted prep time − predicted travel time.
  • Score candidates by predicted pickup arrival vs. ready time, delivery time, and courier fairness.
  • Batch matching: every few seconds, solve an assignment problem across all pending orders and available couriers in a region (bipartite matching), rather than greedily one order at a time. Allow stacking two orders from the same restaurant or route.

3. One order, one courier

Offers time out (e.g. 30–45s). Lock the courier while an offer is outstanding (SET courier_lock:{id} {orderId} NX EX 45, as in Uber), and make acceptance a conditional update on the order (status = 'confirmed' AND courier IS NULL). A declined or expired offer returns the order to the pool.

4. Courier locations

~250k pings/second go to an in-memory geo index (Redis GEO or geohash buckets sharded by region), not a database row per ping (see Finding What's Nearby). Archive the stream to Kafka for trip history, ETA training and dispute resolution.

5. ETAs

The customer's ETA = remaining prep time + courier travel to restaurant + pickup wait + travel to the customer. Each piece is predicted by models trained on history (restaurant prep times by hour and order size, travel times from routing + traffic, parking/handoff times per building). Update the ETA on each state change and when the courier's progress deviates.

6. Scaling by geography

Orders, couriers and restaurants interact within a city. Partition dispatch, location indexes and event streams by region so each region's matching runs independently, and a spike in one city doesn't affect another.

[!WARNING] Don't lose orders between services Write the order and its outbox event in one transaction (transactional outbox), and publish the event from the outbox. Otherwise a crash between "order saved" and "event published" leaves an order no courier is ever looking for.

What's expected at each level

  • Mid-level: order placement, restaurant acceptance, finding a nearby courier, tracking via location updates.
  • Senior: an order state machine with conditional transitions, offers with locks and timeouts, an in-memory geo index, timing dispatch with prep time.
  • Staff: batch assignment and order stacking, ETA decomposition into predicted components, regional partitioning, outbox-based event reliability and marketplace trade-offs (customer speed vs. courier earnings).