SysDesignPrep.com
System design interview question

Design DoorDash

Take a food order, get a restaurant to cook it and a courier to arrive just as it is ready, and keep three parties in sync across millions of orders a day.

Last updated 2026-09-30. Difficulty: hard. Patterns: state-machine, dispatch, eta-prediction, location-tracking, marketplace. Reported at DoorDash, Uber, Instacart, Amazon, Lyft.

Walk through a strong candidate's answer, turn by turn.

The interviewer asks, the candidate answers and draws, and you press Next. Pause to answer yourself at the key decisions, and ask the AI Mentor anything along the way.

Functional requirements

  • Browse and order. See restaurants that deliver to my address and are open, pick items from a menu, pay, and get an estimated delivery time.
  • Restaurant workflow. The restaurant’s tablet receives the order, accepts it with a prep time, and marks it ready.
  • Dispatch. Assign a courier so they arrive at the restaurant when the food is ready, sometimes carrying two orders at once.
  • Live tracking. The customer sees the order’s status and the courier on a map, with an ETA that updates.
  • Payments. Authorise at checkout, capture on delivery, refund on problems, pay restaurants and couriers.
  • Out of scope. Menu management tooling, courier onboarding and background checks, ads, and the grocery business.

Non-functional requirements

  • Scale (5 M orders/day · ~300 orders/s at dinner peak). Order volume is modest by web standards; the difficulty is the coordination each order needs.
  • Correct order state (every order reaches a final state). An order must never be lost, charged twice, or left stuck between parties. Each transition is durable and idempotent, and every waiting state has a timeout.
  • Dispatch quality (food waits < 5 min, couriers wait < 5 min). This is the requirement that drives the hardest trade-off: assigning instantly to the nearest courier is simple but wastes couriers and lets food go cold; waiting to batch and time assignments is better overall but makes each decision slower and harder.
  • Location updates (~100 k/s at peak). Hundreds of thousands of couriers report every few seconds. Only the latest position matters for dispatch; the history matters for ETAs and disputes.
  • ETA accuracy (p80 within ± 10 min). The promised time drives customer satisfaction and the dispatch plan; it is a prediction problem, not a calculation.
  • Availability (99.95 % for ordering and tracking). Dinner is short. An outage at 7 p.m. is a large share of a day’s revenue.

Back-of-envelope estimates

  • Orders per second: ~60 avg · ~300 peak. 5 M orders a day ÷ 86 400 ≈ 58/s; dinner concentrates a large share into a few hours, about 5× ≈ 300/s.
  • Courier location updates: ~100 k/s. 500 k couriers online at peak, each sending a position every 5 s = 100 k updates/s. Small payloads, but the most frequent write in the system.
  • Orders in flight at peak: ~720 k. 300 orders/s × ~40 minutes from order to delivery (2,400 s) ≈ 720 k by Little’s law. Roughly a quarter of them (about 200 k) are waiting for a courier or pickup at any instant: that is the set dispatch is actively planning.
  • Dispatch decisions per region: ~every 30 s. Running the optimiser for each region (a city or part of one) every 30 seconds over the waiting orders and nearby couriers trades a few seconds of delay for much better assignments than one-at-a-time greedy matching.
  • State transitions per order: ~10. Placed, authorised, sent to store, accepted, courier assigned, arrived at store, picked up, arriving, delivered, captured: about 10 durable transitions per order, so 3 k/s at peak, each emitting an event.

Components

  • Customer app: Browses stores, places orders with an idempotency key, and follows the order and the courier on a map over a live connection.
  • Restaurant tablet: Receives orders, confirms them with a prep time, and marks them ready. Often on unreliable restaurant Wi-Fi, so it acknowledges every order explicitly and the platform escalates by phone if it does not.
  • Courier app: Sends location every few seconds, receives delivery offers with a short window to accept, and walks through pickup and drop-off steps.
  • Tracking service (WebSocket / SSE): Pushes order status, courier position (smoothed and delayed slightly) and the updated ETA to the customer.
  • API gateway: Authenticates the three kinds of clients and routes their requests. Separate rate limits for each.
  • Store search (geo + availability): Finds stores whose delivery area covers the address and that are open and accepting orders, ranked with an estimated delivery time per store.
  • Order service (state machine): Owns each order’s state machine: validates and prices the cart, authorises payment, notifies the restaurant, and records every transition durably with timeouts on every waiting state.
  • Order store (Postgres · sharded by order id): Orders, items, state, state history and timers. Each transition is a conditional update from the expected previous state, so transitions are idempotent and race-free.
  • Payment provider: Authorises the card at checkout, captures the final amount (with tip) on delivery, and refunds when something goes wrong. See Design a Payment System.
  • Dispatch optimiser (per region, every ~30 s): For each region, periodically solves which courier should take which order (and which orders can share a courier), timing assignments so couriers arrive as food is ready, then sends offers.
  • Order events (Kafka): Every order transition and courier action as an event. Feeds dispatch, tracking, notifications, ETA training and analytics.
  • ETA models (prep · travel · delivery): Predict prep time per store and order, travel time for couriers, and end-to-end delivery time. Used for the promised time, dispatch timing and live updates.
  • Location service: Ingests courier positions, keeps the latest per courier in memory for dispatch and tracking, and streams the history for ETAs and disputes.
  • Courier positions (in-memory · H3 cells): Latest position, status and capacity of every online courier, indexed by hexagonal cell so dispatch can find couriers near a store instantly.

User flows

  1. Place an order. From browsing to a confirmed order: find stores that can deliver, price the cart, authorise payment, and hand the order to the restaurant, durably and exactly once.
    1. The customer browses stores that deliver to their address and are open now. Each store has a delivery polygon; search finds stores whose polygon contains the address, filters by open and accepting, and shows a predicted delivery time per store. Stores that are overloaded (long prep times right now) are ranked lower or paused.
    2. The customer places the order with an idempotency key.
    3. The order service prices the cart and authorises the payment. Authorise, not charge: the final amount can change (substitutions, tip changes), and an order the restaurant rejects is a voided authorisation rather than a refund.
    4. It records the order as "sent to store" and publishes the transition. The state change and the event are written together (transactional outbox), so the restaurant and dispatch can never miss an order the database accepted. A timer is set: if the store has not acknowledged within 2 minutes, escalate.
    5. The restaurant tablet receives the order and accepts it with a prep time.
  2. Assign a courier who arrives as the food is ready. Dispatch is a periodic optimisation per region, not first-come matching: it decides who, which orders together, and when to send them.
    1. Accepted orders appear on the event stream for their region. Dispatch keeps the set of unassigned and soon-to-be-ready orders per region in memory, rebuilt from the stream on restart.
    2. Every ~30 seconds the optimiser gathers nearby couriers and predicted ready times. For each order: couriers within a few H3 cells of the store, their current task and capacity, travel time to the store and on to the customer, and the predicted moment the food will be ready.
    3. It solves the assignment, including batching two orders on one courier when it helps. The objective balances delivery time, food waiting, courier waiting and courier efficiency. Orders whose food will not be ready for 20 minutes are often left unassigned this round, because the best courier for them is not knowable yet.
    4. It sends an offer to the chosen courier, who has a short window to accept.
    5. On acceptance the order moves to "courier assigned"; the customer’s ETA is updated.
  3. Friday dinner rush. The scale case: orders jump 5×, couriers are scarce, restaurants slow down, and location updates hit 100 k/s.
    1. Courier positions arrive at 100 k/s; only the latest per courier is kept hot. Each update overwrites one in-memory entry and moves the courier between H3 cells if needed. The history goes to the event stream for ETA training, not to a database row per update.
    2. Restaurants get slower; prep-time predictions stretch and dispatch waits longer before assigning. Sending a courier on the original schedule would leave them waiting at the counter, which wastes the scarcest resource on the busiest night. Prep predictions use the store’s live order queue.
    3. Dispatch batches more aggressively: one courier, two orders from the same or nearby stores. Batching raises deliveries per courier-hour when supply is short, at the cost of a few minutes for the second customer. The optimiser only batches when the predicted delay stays within the promise.
    4. Search shows longer ETAs and higher fees, and pauses overloaded stores. Raising delivery fees and couriers’ pay in busy zones brings more couriers online and moves demand to times or stores with capacity. Honest ETAs at checkout prevent promises the system cannot keep.
  4. The courier declines, the restaurant is late, a tablet goes silent. The failure path: every waiting state has a timer, and every timeout has a defined next step, so no order is ever stuck.
    1. The courier lets the offer expire; the order returns to the pool for the next round. The offer’s expiry is a durable timer. The courier’s decline history feeds future offers (do not keep offering long trips to someone who always declines them).
    2. The tablet has not acknowledged the order after 2 minutes; the platform escalates. Escalation is a ladder: resend, then an automated phone call to the restaurant, then cancel and void the authorisation with an apology credit. Each step is a timed state transition.
    3. The restaurant marks the order late; ETAs and the courier’s timing are updated. The courier can be reassigned to something else meanwhile rather than wait. The customer sees the honest new time.
    4. If the order must be cancelled, the authorisation is voided or a refund issued. Cancellation is a state with rules: who pays depends on how far the order got (food already cooked, courier already en route). The payment action uses the order id as idempotency key, so retries cannot refund twice.
  5. Track and deliver. The last mile: live tracking, the delivery step, and settling the money.
    1. The customer’s app shows status and the courier on the map. The tracking service reads the courier’s latest position every few seconds and pushes a smoothed, slightly delayed position, so the dot moves along roads and the courier’s exact location is not exposed in real time.
    2. The courier picks up, drives, and marks the order delivered with a photo or code.
    3. The order service captures the payment, including the final tip.
    4. Events update courier and restaurant earnings and train the ETA models. Every delivery is a labelled example: predicted versus actual prep, travel and handoff times, per store and area.

Deep dives

An order is a state machine across three parties

How do you keep an order consistent between customer, restaurant and courier, when any of them can fail or go silent?

An order passes through about ten steps owned by different parties: the customer pays, the restaurant accepts and cooks, a courier is found, picks up and delivers, and money moves at the end. Each party’s app can crash, retry, or lose connectivity at any point, and messages can arrive twice or out of order.

Without an explicit model, orders get stuck ("accepted" but no courier ever assigned), double-transition (assigned to two couriers), or quietly disappear. The fix is to make every allowed transition explicit, durable and conditional, and to give every waiting state a deadline.

  • Explicit state machine in the order service; conditional transitions; durable timers on waiting states; transactional outbox for events chosen
  • Choreography: each service reacts to events with no central owner situational: secondary reactions (notifications, analytics) around a central owner
  • A workflow engine (Temporal, Step Functions) orchestrating each order situational: teams that want durable workflows without building timer infrastructure

The answer: The order service owns the state machine: placed → authorised → sent_to_store → accepted → courier_assigned → at_store → picked_up → delivered → captured, plus cancelled and failed branches. Each transition is an UPDATE … WHERE state = expected, so duplicates and races are no-ops, and it writes the new state and an outbox event in the same transaction. Every waiting state has a durable timer (store acknowledgement, courier offer, pickup lateness) whose expiry triggers a defined transition such as escalate, reassign or cancel. A reconciler scans for orders that have sat in any state longer than its deadline, as a safety net for lost timers.

Two couriers accept the same offer at the same moment. What happens?

Both requests try UPDATE orders SET courier = X, state = courier_assigned WHERE id = ? AND state = awaiting_courier. One wins; the other affects zero rows and is told the order is taken, then gets another offer. The database decides; no lock is needed.

How do you implement millions of durable timers?

Store deadlines in an indexed column (next_deadline_at) and have workers poll for due rows in small batches, or use a timer service or workflow engine with persistent timers. Either way, firing a timer is just another conditional transition, so firing twice is harmless.

Why an outbox rather than publishing events after the commit?

If the service commits the state change and crashes before publishing, dispatch never hears about the order. Writing the event into an outbox table in the same transaction, and relaying it to Kafka afterwards, guarantees every committed transition is published at least once.

How would you know orders are getting stuck?

A dashboard of orders by state and age, with alerts when the count of orders past their state’s deadline rises. The reconciler’s actions are also a metric: if it has to act often, a timer path is broken.

Dispatch: who, which orders, and when

Why not just assign each order to the nearest free courier the moment it is placed?

Nearest-courier-now is greedy: it ignores that the food will not be ready for 15 minutes (so the courier waits), that another order from the same restaurant is about to arrive (so two couriers make one trip), and that this courier might be far better for an order placed 20 seconds later.

Food delivery adds a constraint ride-hailing does not have: the pickup is not ready until the restaurant finishes cooking. Arriving early wastes courier time; arriving late gives cold food. The best decision depends on predictions of prep and travel time.

  • Periodic batch optimisation per region (every ~30 s) using predicted ready and travel times, with order batching and deferred assignment chosen
  • Greedy nearest available courier at order time situational: very low volume regions, or as a fallback if the optimiser is down
  • Broadcast the order and let couriers claim it rejected

The answer: Each region (a city or part of one) runs an optimiser every ~30 seconds over the orders that are accepted but unassigned and the couriers who are free or finishing soon. Candidate pairs are filtered by distance (a few H3 rings around the store) and scored with predicted courier arrival versus predicted food readiness, delivery time to the customer, and courier efficiency; pairs and two-order batches are chosen by solving an assignment problem. Orders whose food will be ready much later are deliberately not assigned yet. Offers go to couriers with a 30-second window; declines and expiries return orders to the next round. Regions are independent, so dispatch scales by sharding regions across machines. DoorDash has described its dispatch system ("DeepRed") as this kind of prediction-driven optimisation.

The optimiser for a city crashes. What happens to orders?

A standby takes over the region, rebuilding its state from the event stream and the courier position cache within seconds. If it cannot, a simple greedy fallback assigns orders so nothing stalls; quality drops for a few minutes, correctness does not.

How do you decide when to batch two orders?

Compare the plan with and without the batch: total courier time saved against the extra minutes for the second customer and food waiting. Only batch if both deliveries stay within their promised windows. At peak, when couriers are scarce, the saving weighs more.

How do you evaluate a dispatch change?

Switchback experiments: alternate the old and new algorithm by region and time window (rather than by user), because every order competes for the same couriers. Measure delivery time, food wait, courier wait, deliveries per courier-hour and lateness against the promise.

Predicting times

Where do the delivery estimate and the "food ready at" time come from?

The promised delivery time is shown before the customer orders, and dispatch depends on predicted prep time and travel time. All three are uncertain: a kitchen with 20 open orders is slower than the same kitchen at 3 p.m.; parking downtown at 7 p.m. adds minutes; a large order takes longer to prepare.

A simple formula (fixed prep plus distance divided by speed) is wrong in exactly the situations that matter most: busy nights and busy places.

  • Learned models for prep, travel and handoff, using live signals; trained on every delivery chosen
  • Restaurant-entered prep time plus map travel time situational: new stores without history, as a starting point
  • Fixed estimates per category rejected

The answer: Three models: prep time (store history, items, current open orders at that store, time of day, the restaurant’s own estimate as a feature), courier travel (routing engine time corrected by learned per-area, per-hour factors), and handoff time at store and door (parking, apartment buildings). The promised time combines them with a margin chosen so a target share of orders arrive on time. Dispatch uses the same predictions to time assignments. Each completed delivery is a labelled example; models are retrained regularly and monitored for drift per market.

Should you show the customer an optimistic or a conservative time?

A calibrated one: aim for, say, 80 % of orders arriving by the promised time, and update it honestly during delivery. Optimistic promises win orders and then lose customers; very conservative ones lose orders.

How do you get prep-time labels if restaurants do not mark orders ready reliably?

Use the courier’s pickup time and wait at the store as the signal: if a courier arrived at 7:10 and picked up at 7:18, the food was ready around 7:18. Combine it with the restaurant’s ready button where it is used honestly.

How do you detect that the ETA model has drifted?

Track prediction error per market and hour against actual outcomes, every day. A sudden shift (a new traffic pattern, a holiday, a model bug) shows up as a jump in error or bias in one market before customers complain about lateness.

Should the customer's ETA change during delivery?

Yes, honestly and smoothly: recompute from live signals (prep progress, courier position) and update the customer, but avoid jitter by only showing changes beyond a minute or two. An ETA that silently passes is worse than one that moves.

100,000 location updates a second

Couriers report their position every few seconds. Where does that go, and how does dispatch find couriers near a store?

Half a million couriers online at peak, each reporting every 5 seconds, is 100 k writes a second. Writing each to a database row would be a lot of writes for data that is stale five seconds later.

Dispatch and tracking only need the latest position; ETA training and disputes need the history. Those are two different stores with different shapes.

  • Latest position per courier in an in-memory, cell-indexed store; history streamed to Kafka and a data lake chosen
  • A row per update in a database rejected
  • Geospatial queries in a relational database situational: small fleets

The answer: The location service accepts batched updates from courier apps, validates them (speed limits, jumps), and writes the latest position, status and capacity per courier into an in-memory store sharded by region, indexed by H3 hexagonal cell. Dispatch asks for couriers within k rings of a store’s cell; tracking reads a single courier’s latest position. Every update is also published to Kafka for the trip history, ETA features and disputes. If a shard is lost, it repopulates from the next round of updates within seconds. This is the same pattern as Design Uber’s driver locations.

Why hexagons (H3) rather than squares?

Every neighbour of a hexagon is the same distance from its centre, so "rings" around a cell approximate a circle well, and distances between cells are uniform. With squares, diagonal neighbours are farther than edge neighbours, which skews radius searches.

How do you protect couriers’ privacy on the customer’s map?

Show the courier only after pickup, delay and smooth the position, and stop showing it once delivered. Customers never see the courier’s exact location before they are on the way to them.

What if a courier's GPS is wrong or spoofed?

Validate updates: impossible speeds or jumps are rejected, and positions are cross-checked with the route and with check-in events at the store. Spoofing to look closer to busy stores is a known fraud; consistency checks and pattern detection on the history catch it.

How do you replay a delivery for a dispute?

From the location history in the event stream (and the data lake), joined with the order's state transitions and the delivery photo. Keeping the full history out of the hot path makes this cheap to store and still available when needed.

Which stores can deliver to me

A customer opens the app. How do you list the stores that can deliver to their address right now?

Delivery areas are not circles: they depend on roads, rivers and how far couriers can travel in reasonable time, so each store has a polygon (or a set of H3 cells). Availability changes constantly: stores open and close, pause when overloaded, and delivery fees change with demand.

The answer must also show a believable delivery time per store, which depends on the store’s current load and courier supply nearby.

  • Precompute each store’s delivery area as H3 cells; index cell → stores; filter by live availability; add predicted ETA chosen
  • Point-in-polygon search for every store nearby situational: as a precise check on the shortlist
  • Fixed radius around each store rejected

The answer: Each store’s delivery area is stored as a set of H3 cells, recomputed from travel-time analysis when it changes. A cell → store ids index answers "who delivers here" with one lookup on the address’s cell. Results are filtered by live status (open, accepting, not paused) from a fast cache updated by store events, enriched with a predicted delivery time and current fee, then ranked by relevance, rating and ETA. Store pages and menus are cached heavily, since they change rarely.

A store is overwhelmed with orders. What should search do?

Lengthen its displayed ETA from the live prep prediction and, past a threshold, pause it automatically for new orders. Sending it more orders only makes every customer’s food later.

How often do delivery areas change?

Rarely as a base, but they can shrink dynamically at peak when couriers are scarce, so far-away customers do not get orders that cannot be delivered in time. Dynamic shrinking is an overlay on the precomputed cells, not a recomputation.

How do you rank stores in search?

Blend relevance to the query, the store's rating and conversion rate, predicted delivery time and fee, and personal history. Stores that are slow right now drop down naturally because their ETA is longer.

Should menus be cached?

Yes: menus change rarely, so cache them per store with a version and invalidate on edit. Item availability (sold out) is a small live overlay so the cached menu does not need rebuilding each time a dish runs out.

The theory behind it

  • Geospatial indexing and proximity search: Geohash, S2 and quadtrees, nearest-neighbour and radius queries, indexing moving objects, map tiles, and the accuracy and hot-cell problems that come with each.
  • Distributed transactions and idempotency: Why cross-service transactions are hard, two-phase commit vs sagas, the outbox pattern, idempotency keys, exactly-once semantics, and how to keep money and inventory correct.
  • Real-time systems: WebSockets, SSE and push: Long polling vs Server-Sent Events vs WebSockets, connection servers and registries, pub/sub fan-out, presence, reconnection and resync, and scaling to millions of connections.

Related

Open in your browser to sign in

Google does not allow sign-in inside this app's built-in browser. Open this page in Safari and sign in there. The link opens this same page.

Tap the ⋯ or share button at the top or bottom of the screen, then Open in browser. Or copy the link and paste it into Safari.