"Design DoorDash" looks like a ride-hailing question with food attached, and that assumption is the fastest way to give a shallow answer. Ride-hailing is a two-sided marketplace: match a rider to a nearby driver, and the trip begins the moment the driver arrives. Food delivery is a three-sided marketplace — customer, merchant, courier — and it inserts a production step that nobody on the platform controls: the restaurant has to actually cook the food. If you want the pure matching-and-tracking version of this problem, read Design Uber; this walkthrough deliberately spends its time on what food delivery adds. Keep the system design interview cheat sheet nearby for the shared primitives, and mirror this structure in a real system design interview.
That third side changes almost everything downstream. Dispatch cannot simply grab the closest courier, because a courier who arrives ten minutes before the food is ready is a courier standing idle during the dinner rush. The ETA is not one travel estimate but a sum of legs, one of which is a kitchen. And unlike a rider, a bag of food can share a vehicle — so one courier can carry several orders at once, which is where most of the platform's unit economics live.
1. Clarify the requirements
Functional requirements
- Customers browse nearby restaurants, see accurate menus and item availability, and place an order.
- Merchants receive, confirm, and mark orders ready; they can pause ordering or mark items sold out.
- The platform assigns a courier, who picks the order up and delivers it.
- All three parties see live status and a delivery ETA.
- Payments are authorised at checkout and captured on completion; refunds handle missing or wrong items.
Non-functional requirements
- Correctness over raw latency for orders — an order is money and food; it must never be silently lost or double-charged.
- Freshness — minimise the gap between food ready and food collected.
- High availability during narrow, brutal peaks (lunch and dinner windows).
- Scale — assume millions of daily orders and hundreds of thousands of concurrent courier location updates.
State your assumptions out loud: one city region at a time, delivery only (no pickup), and courier locations refreshed every few seconds.
2. The order lifecycle as a state machine
The single most useful thing you can put on the whiteboard early is the order state machine. It makes the whole system legible, and it is where most failure handling lives.
CREATED -> PAYMENT_AUTHORIZED -> SENT_TO_MERCHANT
|
+---------------+---------------+
v v
CONFIRMED REJECTED -> REFUNDED
|
v
PREPARING ---(courier assigned in parallel)
|
v
READY_FOR_PICKUP
|
v
PICKED_UP -> EN_ROUTE -> DELIVERED -> CAPTURED
|
+--> CANCELLED (policy: who pays?) -> REFUNDED
Model this as a persisted state machine with explicit allowed transitions, not as a pile of boolean columns. Every transition is written to an append-only event log, which gives you an audit trail for disputes, a stream for the tracking service, and replayability. Transitions must be idempotent: merchant tablets on flaky restaurant Wi-Fi will retry, and a duplicate "confirm" must not create a second order. Use an order ID plus a client-supplied idempotency key on every write.
Cancellation deserves a dedicated answer because it is genuinely three-sided: a cancel before CONFIRMED is cheap, a cancel after PREPARING means someone eats the cost of the food, and a cancel after PICKED_UP mostly becomes a refund decision. Say explicitly that policy, not the state machine, decides who pays.
3. Menus, availability, and restaurant search
Menus are read-heavy and write-light, so they cache well — but availability is the opposite. A merchant marking an item sold out, or pausing orders because the kitchen is swamped, must propagate in seconds, or customers order food that will never arrive. Split the two: version the menu document and serve it from a CDN or distributed cache, while keeping a small, fast-changing availability record that the storefront checks at render and again at checkout.
Discovery is a geospatial query: given the customer's location, find open merchants that deliver to that address. Index merchant coordinates with a geohash, quadtree, or S2 cell scheme so proximity becomes a prefix or range lookup rather than a full scan. Then filter — open now, address inside the delivery zone, items in stock — and rank by relevance, rating, and estimated delivery time. Note that the ranked ETA shown in the list is a cheaper, approximate version of the ETA you will commit to at checkout.
4. High-level architecture
[Customer app] [Merchant tablet] [Courier app]
| | |
+--------- API gateway / auth --------+
|
+--------+-------------+-------------+-----------+
| | | | |
Search Order Merchant Dispatch Location
service service service service ingest
| | | | |
Geo Order Menu + Assignment Courier
index store + availability + batching positions
event log | | (in-memory
| | | | geo store)
+--------+------+------+------+------+-----------+
| |
[ Event bus ] [ ETA / ML service ]
| ^
+----------+----------+ | features
v v v |
Tracking Payments Notifications <- feature store
(live) + refunds (push/SMS)
The order service owns the source of truth and emits events; everything else subscribes. Location ingest is deliberately separate from the order path — it is a firehose of small, lossy-tolerant writes that belongs in an in-memory geospatial store, not in your transactional database.
5. Prep-time estimation
Prep time is the variable that makes this problem distinctive, and it is a prediction, not a lookup. Useful signals include the merchant's historical time-to-ready, the item mix and order size, current kitchen load (how many orders that merchant has in flight), time of day and day of week, and whether the merchant confirms orders promptly. Merchants who use a tablet that reports "food ready" give you a ground-truth label; merchants who do not force you to infer readiness from courier pickup timestamps, which is noisier.
Be candid about the uncertainty. Prep time has a long tail, and the cost of error is asymmetric on both sides: too early and the courier idles, too late and the food cools. Most designs therefore work with a predicted distribution and pick an operating point rather than treating a point estimate as truth.
6. Dispatch: assignment and batching
Dispatch is the heart of the answer. The naive approach — assign the nearest available courier the instant the order is confirmed — fails for the reason above. Instead, run dispatch as a repeated batch optimisation: every few seconds, take the set of pending orders and the set of available couriers in a region and solve an assignment problem over that window, rather than greedily matching one order at a time. Delaying assignment slightly gives the optimiser more supply and more orders to work with, which is why "wait a bit before assigning" is a feature, not a bug.
The cost function is multi-objective. Typical terms: courier idle time at the merchant, food-wait time after ready, total delivery duration versus the promised ETA, courier travel distance (their earnings and your incentive spend), and fairness across couriers. Constraints include vehicle type, order size, and any age- or licence-restricted items.
Batching is the lever that changes unit economics. If two orders have pickups and drop-offs that are close together in space and time, one courier can carry both, raising deliveries per courier-hour. The dispatcher enumerates candidate routes over the combined pickup and drop-off points and accepts a batch only if the extra detour stays inside a per-customer delay budget. Cap batch size — every additional stop compounds risk against an ETA you already promised — and prefer batching two orders from the same merchant, which costs almost nothing extra.
| Dispatch choice | Wins | Costs |
|---|---|---|
| Greedy nearest courier | Simple, instant | Idle couriers, poor global utilisation |
| Windowed batch optimisation | Better global assignment | Added latency, more complex |
| Order batching (multi-drop) | Higher deliveries per hour | ETA risk for the first customer |
7. Live tracking
Courier apps emit location every few seconds. Ingest through a lightweight endpoint into a partitioned in-memory geospatial store keyed by region, keep only the recent trail, and archive the rest asynchronously for analytics. Fan out to the customer's map over a persistent connection (WebSocket or server-sent events) with polling as the fallback, and smooth the path by snapping points to the road network so the marker does not jitter. Backpressure matters: at peak you would rather drop an intermediate position update than delay the whole stream.
8. ETA modelling
The customer sees one number, but it is a composition:
ETA = merchant_confirm + prep_time
+ courier_travel_to_merchant
+ wait_and_pickup_at_merchant
+ travel_to_customer
+ dropoff (parking, elevator, handoff)
Each leg is predicted separately, then combined — and because the legs overlap (the courier travels while the food cooks), the pickup moment is roughly the max of "food ready" and "courier arrives", not the sum. Quote a range rather than a single minute, and add a buffer: lateness hurts far more than an early arrival. Re-estimate continuously as the order progresses and push updates, because a silently wrong ETA generates support contacts, which are expensive.
9. Pricing, payments, and refunds
Pricing has several components: item prices set by the merchant, a delivery fee that varies with distance and current supply and demand, service fees, and courier incentives during undersupply. At a high level, surge-style dynamic pricing exists to balance a marketplace — when demand outruns available couriers, raising fees and courier pay pulls supply onto the road and shifts some demand later. Say that, and say that you would cap and clearly disclose it; do not pretend to know any company's exact formula.
Payments follow authorise at checkout, capture on delivery, which handles the common case where the final total shifts (a substituted item, an added tip). Treat the payment flow as a saga across order, payment, and merchant payout services with compensating actions rather than a distributed transaction, and make every step idempotent against a payment intent ID. Refunds are their own workflow: partial refunds for missing items, full refunds for undelivered orders, with automated approval under a threshold and manual review above it to limit abuse.
10. Scale and trade-offs
- Shard by region. Dispatch is inherently local — a courier in one city never serves another — so partition orders, couriers, and the dispatch optimiser by geographic region. This is the cleanest scaling axis in the whole design.
- Peaks, not averages. Traffic is concentrated in two daily windows; size and autoscale for the peak and degrade gracefully (longer quoted ETAs, temporary batching increases) rather than failing.
- Separate the hot paths. Order writes need durability and exactly-once semantics; location updates need throughput and tolerate loss. Different stores, different guarantees.
- Optimisation latency vs quality. A longer dispatch window produces better assignments but delays every order. This trade-off is the single best thing to volunteer unprompted.
- Merchant reliability is a first-class input. Slow-confirming or frequently-late merchants should influence both the ETA and how early you dispatch a courier.
Framework reminder: The arc is the same as every design answer — requirements → core entity and lifecycle (the order state machine) → the hard subproblem (prep-time-aware dispatch and batching) → supporting services → trade-offs. The pieces recur elsewhere: geospatial indexing and live tracking in Design Uber, and ranking and prediction in Design a Recommendation System.
Practice distributed systems with live AI support
CoPilot Interview surfaces a structured design skeleton — requirements, entities, the core algorithm, and the trade-offs worth naming — in about 4 seconds during real Zoom and Teams calls. Free tier for Windows and macOS. Explore the free AI interview assistant.
Download freeFAQ
How is designing DoorDash different from designing Uber?
Ride-hailing is a two-sided marketplace that matches a rider to a nearby driver, and the trip starts as soon as the driver arrives. Food delivery is a three-sided marketplace involving the customer, the merchant, and the courier, and it adds a production step: the restaurant has to cook the food. That single difference drives most of the design, because dispatch must time the courier's arrival to the predicted prep completion, the ETA is a sum of prep, pickup and travel legs, and one courier can carry several orders at once.
Why does courier assignment depend on food prep time and not just proximity?
Assigning the closest courier immediately makes them wait idle at the restaurant until the food is ready, which wastes supply during peak hours. Assigning too late leaves the food sitting on the pass getting cold. The dispatcher therefore predicts when the order will be ready and picks a courier whose estimated arrival at the merchant lands close to that moment, optimising a cost function over wait time, food freshness, and total delivery duration rather than distance alone.
How does batching multiple orders to one courier work?
Batching assigns two or more orders to a single courier when their pickups and drop-offs are close enough that the combined route costs little extra time. The dispatcher searches candidate routes over the pickup and drop-off points, and accepts a batch only when the added detour stays inside a delay budget for each customer. Batching raises deliveries per courier-hour and lowers cost per order, but it must be capped because every extra stop adds risk to the promised ETA.
How is the delivery ETA calculated?
The quoted ETA is a composition of several predicted legs: time for the merchant to confirm and prepare the order, courier travel time to the restaurant, wait and pickup time at the merchant, travel time to the customer, and drop-off time including parking and building access. Each leg is a separate prediction fed by historical data for that merchant, item mix, time of day, and traffic. Platforms usually quote a range and add a buffer, because customers dislike a late delivery far more than an early one.
How do you find nearby restaurants efficiently?
Restaurant discovery is a geospatial query: given the customer's location, return open merchants that deliver to that address. Encoding coordinates with a geohash, quadtree, or S2 cell index turns the two-dimensional proximity problem into a prefix or range lookup that a database can serve quickly. Results are then filtered by whether the merchant is currently open and staffed, whether the address falls inside its delivery zone, and whether items are in stock, before ranking by relevance, rating, and estimated delivery time.