Design DoorDash
Run this for someone else. You hold the answers; they do not. Read the prompt, keep the clock, and use the probes below when an answer is thin. Do not show them this page.
Open with this
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. Take a couple of minutes on requirements, then we will do some numbers, then the design. I will interrupt to keep us moving.
The clock
- 4 min: functional requirements and scope
- 4 min: non-functional requirements, with numbers
- 5 min: back-of-envelope estimates
- 16 min: high-level design and one or two flows
- 16 min: deep dives and the close
Move them on out loud when a section overruns. The commonest failure is spending twenty minutes on requirements and never reaching a deep dive, and preventing that is your job as much as theirs.
Requirements · 8 min
Listen for: a scoped set of capabilities, an explicit out-of-scope list, and numeric targets rather than adjectives. Prompt with “what are you not building?” if they never scope, and “what number would make that requirement real?” if they say “fast” or “highly available”.
Functional (6)
- 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 (6)
- 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.
Estimates · 5 min
Ask for two or three numbers, not all of them. What matters is whether they state assumptions, round sensibly, and say what the number implies. Push once with “where did that come from?”
The numbers (5)
- 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.
High-level design · 16 min
Let them draw. Interrupt only to ask what backs a component or what a box actually does. Then pick one flow below and ask them to walk it end to end.
Components (14)
- 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.
Flows to ask them to walk (5)
- 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.
- The customer browses stores that deliver to their address and are open now.
- The customer places the order with an idempotency key.
- The order service prices the cart and authorises the payment.
- It records the order as "sent to store" and publishes the transition.
- The restaurant tablet receives the order and accepts it with a prep time.
- 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.
- Accepted orders appear on the event stream for their region.
- Every ~30 seconds the optimiser gathers nearby couriers and predicted ready times.
- It solves the assignment, including batching two orders on one courier when it helps.
- It sends an offer to the chosen courier, who has a short window to accept.
- On acceptance the order moves to "courier assigned"; the customer’s ETA is updated.
- Friday dinner rush: The scale case: orders jump 5×, couriers are scarce, restaurants slow down, and location updates hit 100 k/s.
- Courier positions arrive at 100 k/s; only the latest per courier is kept hot.
- Restaurants get slower; prep-time predictions stretch and dispatch waits longer before assigning.
- Dispatch batches more aggressively: one courier, two orders from the same or nearby stores.
- Search shows longer ETAs and higher fees, and pauses overloaded stores.
- 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.
- The courier lets the offer expire; the order returns to the pool for the next round.
- The tablet has not acknowledged the order after 2 minutes; the platform escalates.
- The restaurant marks the order late; ETAs and the courier’s timing are updated.
- If the order must be cancelled, the authorisation is voided or a refund issued.
- Track and deliver: The last mile: live tracking, the delivery step, and settling the money.
- The customer’s app shows status and the courier on the map.
- The courier picks up, drives, and marks the order delivered with a photo or code.
- The order service captures the payment, including the final tip.
- Events update courier and restaurant earnings and train the ETA models.
Deep dives · 16 min
Pick two. Ask the headline question, let them answer, then use the follow-ups. The follow-ups are where the level gets decided, so leave time for at least three of them.
An order is a state machine across three parties
Ask: How do you keep an order consistent between customer, restaurant and courier, when any of them can fail or go silent?
Good answers name: Explicit state machine in the order service; conditional transitions; durable timers on waiting states; transactional outbox for events, Choreography: each service reacts to events with no central owner, A workflow engine (Temporal, Step Functions) orchestrating each order.
Our pick: 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
Ask: Why not just assign each order to the nearest free courier the moment it is placed?
Good answers name: Periodic batch optimisation per region (every ~30 s) using predicted ready and travel times, with order batching and deferred assignment, Greedy nearest available courier at order time, Broadcast the order and let couriers claim it.
Our pick: 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
Ask: Where do the delivery estimate and the "food ready at" time come from?
Good answers name: Learned models for prep, travel and handoff, using live signals; trained on every delivery, Restaurant-entered prep time plus map travel time, Fixed estimates per category.
Our pick: 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
Ask: Couriers report their position every few seconds. Where does that go, and how does dispatch find couriers near a store?
Good answers name: Latest position per courier in an in-memory, cell-indexed store; history streamed to Kafka and a data lake, A row per update in a database, Geospatial queries in a relational database.
Our pick: 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
Ask: A customer opens the app. How do you list the stores that can deliver to their address right now?
Good answers name: Precompute each store’s delivery area as H3 cells; index cell → stores; filter by live availability; add predicted ETA, Point-in-polygon search for every store nearby, Fixed radius around each store.
Our pick: 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.
Close · 5 min
Ask what breaks first at ten times the load, and what they would build next. Then give them your read: one thing that was strong, one thing that was missing, one thing to practise. Be specific; “good job” helps nobody.