Design Order & Dispatch
- Scope the food order as a three-sided problem: consumer, merchant, and Dasher at once
- Walk the order and dispatch paths end to end, then defend where canonical state lives
- Work the order state machine, dispatch timing, batching, and how the three sides stay in sync
Understanding the problem
Design the system behind a DoorDash order: a consumer places food, the merchant cooks it, and a Dasher delivers it. Take one order from checkout all the way to the customer's door, across all three parties.
This is the canonical big-box of a DoorDash system design round, and it is the one that exposes whether you understand the product. Every other delivery design is a slice of it. The arc is short to say and hard to build. A consumer places a food order. The system accepts it and charges them, then hands a ticket to the merchant so the kitchen starts cooking. At roughly the same time it goes hunting for a Dasher to carry the food. It offers the delivery to a good one and tracks the whole thing until the bag is at the door. Three parties and one order, and none of them is under your control.
The thing that makes this different from a ride is the kitchen. An Uber match pairs a rider with a driver and the trip starts. A DoorDash order has a third actor in the middle who takes an unpredictable amount of time to make the food, and the whole system has to bend around that. Dispatch a Dasher too early and they stand at the counter for ten minutes while supply you paid for goes to waste. Dispatch too late and the food sits under a heat lamp going cold while the consumer watches a stalled map. The merchant guide calls this out: DoorDash wants engineers who think like operators, and the timing problem is where that shows.
Put rough numbers down early, because they decide what is genuinely hard here. DoorDash runs on the order of a couple billion orders a year. Call it 2.5 billion, which is about 7 million orders a day, and about 80 orders a second on a flat average. Orders are not flat. They pile into the dinner window, so the real peak is a few hundred a second on a normal evening and into the low thousands on a Friday night or a big game day. That is a modest write rate. A single database could keep up with the orders alone. The hard part is the coordination. Each order is a small, long-running handoff between three parties who each fail in their own way. The same order fans out into notifications and tracking, plus payment and analytics that multiply the traffic.
Functional requirements
Stake out a tight scope and let the interviewer redirect. The core is the lifecycle of one order across three parties. A consumer places an order and pays. The merchant receives it and works the kitchen. The system finds and assigns a Dasher, and everyone tracks the order to delivery or to a clean cancellation.
In scope
- Place an order with a merchant and charge the consumer once
- Push the order to the merchant and track the kitchen through prep
- Find an available Dasher near the store and offer the delivery
- Drive the order through its lifecycle to delivered or cancelled
- Batch two nearby orders onto one Dasher when it makes sense
Below the line (name it, then skip)
- The dispatch optimizer itself (its own design, deep dive points at it)
- ETA prediction and live location tracking (its own design)
- Payment internals and Dasher payouts (its own design)
- Restaurant search, menus, and recommendations
- Ratings, reviews, and the support tooling
Non-functional requirements
Reason through each target, because the requirements are the design here. The order is the consistency-critical object. Once a consumer sees "order placed" and a card is charged, that order cannot quietly vanish, cannot be charged twice, and cannot have two Dashers show up for it. So the canonical order state is strongly consistent, and every transition through its lifecycle is exactly-once. We would rather reject a state change and retry than let an order skip a step or run a step twice.
Dispatch is the opposite kind of problem, and saying so scores. Picking which Dasher gets an order tolerates being a beat behind and tolerates being approximate. A slightly stale view of who is free near a store costs at most a wasted offer that the next Dasher picks up. That relaxation is what lets dispatch run as its own asynchronous service instead of blocking the order. Availability matters everywhere, because a checkout that times out at dinner is a lost order, but the order's correctness wins whenever the two collide. One more property shapes everything downstream: the marketplace is naturally regional. An order in Chicago is only ever matched to a Dasher in Chicago, so the entire system shards cleanly by market, and no single shard ever carries the whole load.
- ~7M Orders / day. ~80/s average, a few hundred/s at dinner peak.
- Exactly 1 Charge and Dasher per order. The invariant the round tests.
- Strong Order-state consistency. Dispatch can be stale; the order cannot.
- Per-market Sharding boundary. A Chicago order never sees a Miami Dasher.
The set-up
Core entities
Name the objects the system moves around before drawing a box. Four carry this design, and the order is the spine that ties the other three together.
Order
One purchase: a consumer, a merchant, the line items and the total, plus a lifecycle state that walks from created to delivered. This is the consistency-critical object, owned by one service, and the thing every other party reads and updates through.
Merchant
The store that makes the food: an id, a location, an open/closed flag, and a prep-time profile built from history. The merchant accepts or rejects an incoming order and signals when the food is ready. Its prep time is the uncertainty the whole dispatch bends around.
Dasher
The delivery driver: an id, an online/offline and free/on-a-delivery status, and a live location that changes every few seconds. The supply side of the marketplace. A Dasher can be offered an order and accept or decline it. They carry one or two deliveries at a time.
Assignment
The link between an order and the Dasher carrying it: which Dasher, when it was offered, whether it was accepted or declined, plus whether this delivery is batched with another. Separating it from the order lets one Dasher hold a batch of two orders without tangling either order's own lifecycle.
One split decides where things live. The Order and its Assignment want a strongly consistent, transactional store. The invariants we care about, one charge and one Dasher per order, are what a row lock and a conditional update enforce. Postgres is the name everyone in the room knows. CockroachDB is the distributed-SQL version DoorDash reaches for when one region's orders outgrow a single node, since it keeps real transactions while spreading across machines. Partition orders by market so a region's orders and assignments sit together and a single order's whole lifecycle is one local transaction. The Dasher live location is the opposite: it changes every few seconds across hundreds of thousands of drivers, a firehose a transactional database would buckle under, so it lives in an in-memory geo store. The high-volume append data, the event history and the analytics trail, goes to Cassandra, which DoorDash runs as its workhorse for write-heavy data at scale. Say those homes out loud. The interviewer asks where each entity lives and why.
API
Keep the surface thin and notice who calls what. The consumer places the order and then subscribes to its status. Merchants get their own endpoints to accept an order and to mark food ready, because the kitchen is a real participant in the flow with its own clock. Dashers are offered deliveries and report progress as they go. The order-placement call carries an idempotency key so a phone that retries on a flaky network still ends up with a single order.
High-level design
Build it one path at a time, simplest version first, naming each bottleneck as it surfaces. The system splits into two paths that meet at the order. One path, the order path, is short and strict: take the order, charge once, hand a ticket to the kitchen, write the canonical state. The other, dispatch, is asynchronous and approximate: find a Dasher, offer the delivery, track it in. An event log sits between them so neither blocks the other. The diagram codes each box by role, blue for the three parties, violet for our services, amber for the event log, green for storage. The walkthrough below tags each component the same way.
1) Place the order. The Consumer calls POST /orders with the cart and a payment, plus an Idempotency-Key. The Order service asks the Payment service to charge the card once under that key, and only on a successful authorization does it write the Order store row in state created. It returns the order id right away so the consumer sees confirmation. This is the strict, synchronous part of the whole design, and it is small. At roughly 80 orders a second on a flat average and a few thousand at the Friday-night peak, one SQL store partitioned by market keeps up without strain. Everything after this can be slow and the order still holds, because the row is already durable. The idempotency key makes a double-tap or a flaky-network retry resolve to one order and one charge instead of two. Keeping that transition exactly-once when the same request arrives twice is deep dive one.
2) Hand the ticket to the kitchen. The Order service hands the order to the Merchant service, which pushes it to the store's tablet or point-of-sale system. The Merchant accepts and the kitchen starts cooking, or rejects, which sends the order down a clean cancel-and-refund path. On acceptance the merchant gives an estimated prep time, usually ten to twenty minutes, and the order moves to confirmed then preparing. This is the step a ride-hailing design does not have, and that prep estimate is the clock the dispatch has to read. We deliberately keep the kitchen off the order's write path. The merchant sends its accept and food-ready signals back as their own intents that the order service applies, so a slow or offline tablet delays the cook but never wedges the order itself. What happens when that tablet drops offline is a failure case the technology section answers directly.
3) Find a Dasher, asynchronously. Placing the order runs ahead without waiting on driver supply. The Order service hands the job to the Dispatch service, which runs on its own. It queries the Dasher geo store for free Dashers near the store, a lookup that answers in microseconds against an in-memory geo index rather than touching the order database. It scores the candidates and offers the delivery to a good one over a websocket. The crucial part is timing rather than distance. If the kitchen needs fifteen minutes and the nearest free Dasher is eight minutes away, offering at order time would leave that Dasher idling at the counter for seven minutes of paid supply. So dispatch aims to have the Dasher arrive right as the food is ready, which usually means waiting before it offers instead of offering the instant the order lands. Deep dive two is this timing decision, and deep dive three is batching two orders into one offer.
4) Drive the order to delivery. As the Merchant cooks and the Dasher drives, each milestone is a transition recorded by the Order service: dasher_assigned, ready_for_pickup, picked_up, delivered. Every transition emits exactly one event on the Event log, roughly eight to ten per order over its life. Each fans out to four or five Downstream consumers: the consumer's live map, push notifications to all three parties, a Premium question, plus the analytics warehouse. That whole fan-out is kept off the order's critical path on purpose, so a slow analytics job or a backed-up notification queue can never stall a checkout or a dispatch. Deep dive one is how the order service keeps this lifecycle exactly-once when the same event can be delivered twice.
Size the firehose
The order rate itself is small, and saying so early tells the interviewer where the hard part is not. Seven million orders a day is about 80 a second on a flat average. That climbs to a few hundred a second through the dinner window and into the low thousands on a Friday night or a big game day. An order row is a couple of kilobytes, so even at peak the canonical store takes single-digit megabytes a second of writes, comfortably inside one SQL node per market. Orders kept hot for a few months come to a few terabytes per market, and the long archive of delivered orders rolls off to the append store. None of this strains a database. If the order volume were the whole story, a single Postgres box would do.
The real volume lives in two places that are not the order. First, Dasher location pings: at peak a few hundred thousand Dashers are online, each reporting position every four seconds or so. That lands around 50,000 to 100,000 writes a second, roughly a thousand times the order rate. That firehose is why the geo store is in-memory and never touches the transactional database. Second, the tracking fan-out. Each order emits eight to ten transitions, each read by four or five consumers, and at the dinner peak hundreds of thousands of consumers hold an open websocket watching a live map.
- Order writes: ~80/s avg. A few hundred a second at dinner, low thousands on a peak night. An order row is ~2KB, so even peak writes stay in single-digit MB/s, inside one SQL node partitioned by market.
- Location pings: 50–100K/s. A few hundred thousand Dashers online, each pinging every ~4 seconds. Roughly 1000x the order rate, which is why positions live in an in-memory geo store and never hit the order database.
- Event fan-out: ~8–10 / order. Each order transition writes one Kafka event read by 4-5 consumers: tracking, notifications, payout, analytics. None of it rides the order's critical path.
- Live tracking: 100Ks of sockets. At dinner peak, hundreds of thousands of consumers hold an open websocket on a moving map. The tracking fan-out, not the order write, is the connection-count problem.
Technology choices
Every box so far is a category. In the room, name a real tool for each and be ready for the failure question, because that is where a DoorDash round goes. What happens when the merchant's tablet is offline, when the dispatch service falls behind at dinner, when the same order event is delivered twice?
- The Order service, Merchant service, and Dispatch service run as stateless services behind a load balancer. At DoorDash these are Kotlin services talking gRPC, the shape it landed on after breaking up an older Python monolith. Stateless is the right category because it makes scaling and failure dull in the best way. No server holds anything a given request needs. That means any box can serve any call. You scale by adding boxes to soak up the dinner peak, and a box that dies just falls out of the pool while the load balancer routes around it. They are split into separate services rather than one because they scale on different curves. At 6pm the dispatch service is doing far more work than the merchant service, and pinning both to one fleet would force you to over-provision the quiet service just to scale the busy one. The failure probe, one instance crashing mid-request, costs a single retried call and never a lost order, because the durable state lives in the store, not in the service.
- The Order store is Postgres, moving to CockroachDB when a single market outgrows one node, partitioned by market so a failure blasts one region, never the country. A relational store is the right category because the invariant we care about is a hard constraint. A row lock plus a conditional update (advance the state only if the row is currently in the expected prior state) enforces one charge and one Dasher per order. It all happens inside a transaction that commits whole or rolls back as a unit, a guard no eventually-consistent store gives you for free. When the interviewer asks about a shard's primary dying mid-order, promote a synchronous replica that has every committed write. An in-flight transaction that had not committed simply rolls back, so the caller sees a clean failure and retries against the new primary instead of a half-charged order.
- The Dasher geo store is Redis with its geospatial commands, holding one live point per online Dasher.
GEOADDwrites a Dasher's point, andGEOSEARCHfinds the free ones inside a radius of a store. In-memory is the category because positions change every few seconds across hundreds of thousands of Dashers, the 50,000-to-100,000-writes-a-second firehose from the sizing above. Only memory absorbs a write rate like that cheaply while answering a radius query in microseconds, where a disk-backed transactional store would melt under the writes and crawl on the reads. Positions are disposable, which answers the lost-node question: every Dasher pings again within seconds and the store refills itself, so a dead node costs a few seconds of freshness, never an order. The full location-ping and tracking design lives in the ETA lesson. - The Event log is Kafka, partitioned by order id so one order's events stay ordered. One transition writes one event, and tracking, notifications, payouts, and analytics each read it independently, so a slow analytics job can never wedge a checkout. DoorDash adopted Kafka in 2019 after an earlier message queue failed under load and took checkout and dispatch down with it, the failure a durable, replayable log prevents. A consumer that crashes rewinds to its last offset and replays, and because events are keyed, a replayed transition does not fire a second notification or a second payout. To bridge the gap between committing the order and publishing the event, write the event to an outbox table in the same transaction as the order. Relay it to Kafka after commit, so a crash between the two can never lose or invent a transition.
- The Payment service charges the consumer through an external processor like Stripe, run as a sequence of idempotent steps keyed by the order's idempotency token. Idempotency matters most here because the network between you and a third-party processor is the part you do not control. A charge call can succeed at the processor and then time out before the response gets back to you, and a naive retry would bill the card twice. Keying every call on the order's token means the processor recognizes the retry and returns the original result instead of charging again. The order service does not mark an order placed until the authorization succeeds, so a consumer is never shown a confirmed order they were not charged for. Dasher payouts run later off the event log, and the whole money design, including reconciliation when a charge times out in that ambiguous window, is its own resilient-payments lesson.
Potential deep dives
Most of the signal lives here, and on this prompt the interviewer drives straight at the food-delivery-specific parts. The order lifecycle, the dispatch timing, and batching are the three a ride-hailing design does not have, and they are where a DoorDash round earns its keep.
1) How does the order lifecycle stay exactly-once?
A single order is really two timelines that have to stay in sync: the kitchen's and the road's. The merchant moves the food from accepted to cooking to ready, and the Dasher moves from assigned to arrived to picked-up to delivered. The Order Management Service is the one place that composes both into a single canonical state. Expect the interviewer to probe how that state behaves under retries, duplicate events, and a participant that drops offline. Model the order as an explicit state machine: a fixed set of states with only certain transitions allowed, so the system always knows what is legal next and rejects anything that is not.
Approach
Give the consumer app, the merchant service, and the dispatch service all write access to the order's status column. Each one updates it when its own thing happens: the merchant sets "preparing," the Dasher sets "picked up," and so on. It looks natural because each party knows its own step.
Challenges
With several writers and no single owner, the transitions race. The Dasher's "picked up" write can land before the merchant's "ready" write, leaving an order that was picked up but never marked ready, which breaks every consumer reading the status. A duplicate "delivered" event, replayed after a consumer crash, marks an already-cancelled order delivered. There is no place that knows the legal next state, so illegal transitions just happen. Nobody should ship this, but being able to say why, no single owner and no transition guard, is the point.
Approach
Make the Order service the only writer of order state. Every party sends it an intent ("merchant accepted," "Dasher picked up"), and the service applies the transition itself inside a transaction. The guard: advance the state only if the order is currently in the expected prior state. That guard is a conditional update, the database equivalent of "set state to preparing only if it is still confirmed." An out-of-order or illegal request fails the guard and is rejected instead of corrupting the order.
Challenges
One owner fixes the races, but the same event can still arrive twice, from a Kafka redelivery or a client retry. Naively reapplying it can double-fire whatever the transition triggers, like a second payout. The guard alone does not make a transition idempotent; it makes it ordered. You still need a way to recognize "I have already processed this exact event."
Approach
Keep the single owner and the guard, and add an idempotency record per transition. Each event carries a unique id, and the order service records processed ids in the same transaction that applies the transition. A redelivered event finds its id already recorded and returns the existing result instead of advancing the order again. Processing it once and processing it five times land the same state and fire the side effect once. Pair this with the outbox: the transition and the outgoing event are written in one transaction, so the order state and the event published can never disagree. The order is now a clean state machine where every transition is exactly-once by construction.
Challenges
You carry an idempotency ledger and prune it on a retention window long enough to cover any realistic retry. That is cheap. The subtle part is the parallel Dasher track. Assignment advances on its own timeline during confirmed and preparing, so model it as a separate sub-state on the assignment object that joins the main line at ready-for-pickup. Cramming both timelines into one linear status breaks. Keeping the kitchen track and the road track as two guarded machines that meet at pickup is what keeps a slow kitchen from corrupting the delivery and the other way around.
The async processing lesson covers the outbox pattern and idempotent consumers that sit underneath this.
2) When do we dispatch a Dasher?
A ride-hailing design never has to answer this question, and it is the heart of food delivery. The naive instinct is to find a Dasher the moment the order is placed. That is wrong, and seeing why is most of the dive. Walk the clock. A typical kitchen takes ten to twenty minutes to make an order. A free Dasher might be eight minutes from the store. If you offer the delivery at order time, the Dasher accepts, drives over in eight minutes, and then stands at the counter for ten minutes waiting on food. That idle Dasher is supply you are paying for and locking up, and a lobby full of waiting Dashers is a real operational headache the merchant feels. You want the Dasher to walk in right as the food comes up.
So the target is an offer time rather than a nearest driver. If the food will be ready at some time T, and a candidate Dasher is d minutes of travel away, offer them the delivery around T minus d. They arrive just as the bag is ready. That needs two predictions: how long the kitchen will take, and how long the Dasher will take to get there. Both are uncertain, which is why this is really a probability problem hiding underneath a scheduling one.
Approach
The instant the order is created, query the geo store for the nearest free Dasher and offer them the delivery. Simple, and it feels responsive because a driver is locked in immediately.
Challenges
The Dasher arrives long before the food. They idle at the counter for the whole prep time, ten to twenty minutes of paid supply doing nothing, and they cannot be picking up anyone else's order while they wait. At dinner, when supply is tightest, this is exactly backwards: you burn your scarcest resource standing still. It also clogs the merchant's pickup area with waiting drivers, which they hate. Responsive for one order, ruinous for the marketplace.
Approach
Wait for the merchant to mark the order ready, then find the nearest free Dasher. Now no Dasher idles at the counter, because you only look for one once the food exists. Supply is never locked up waiting on a kitchen.
Challenges
Now the food waits on the Dasher instead. The order is ready and sitting on the shelf, and only then do you start a search that might take a few minutes to find and route a driver, plus their travel time. The food cools, the consumer's ETA blows out, and quality drops. You have moved the waiting from the Dasher to the food, which is worse, because cold food is the thing the consumer feels. Reactive dispatch is always a step behind the kitchen.
Approach
Estimate the food-ready time T from the merchant's prep-time profile and live signals, and estimate each candidate Dasher's travel time d. Offer the delivery so the Dasher arrives right around T, which means starting the offer around T minus d rather than at order time or at food-ready. The Dasher rolls up as the food comes out and the bag goes straight into the car. Neither side waits. Because the estimate is uncertain, aim a little early so a slightly slow Dasher still beats a slightly fast kitchen, and keep re-evaluating as the prep estimate sharpens. This is the just-in-time dispatch that keeps both the food hot and the Dasher productive.
Challenges
It rides entirely on the two predictions, and both are noisy: a kitchen that says fifteen minutes can take thirty on a rush, and traffic moves the travel estimate. So treat T as a distribution, not a point, and lean the offer time toward arriving early, since an idle minute for a Dasher is cheaper than ten minutes of a cooling order. The prep-time prediction itself, and the heavier optimization of which Dasher and in what order, is its own problem. This dive sets up the timing; the Dasher-assignment lesson builds the scoring and the optimizer.
3) How do we batch two orders onto one Dasher?
Batching, sometimes called stacking, is assigning more than one order to a single Dasher on one trip, and it is uniquely a food-delivery problem. A ride pools two passengers only rarely. A delivery network batches constantly, because food orders cluster hard in time and space. At lunch, a dozen orders leave the same food hall in the same ten minutes, headed for the same office towers. The payoff: a Dasher who picks up two orders from one plaza and drops them two blocks apart does roughly two deliveries in the time and miles of about one and a third. That lifts deliveries per Dasher-hour, which is the number that decides whether the marketplace is efficient enough to be affordable.
The cost is the second consumer's patience. Batching adds a detour, so one order in the batch arrives later than it would have alone. The whole craft is capturing the efficiency without making the delayed order noticeably worse.
Approach
Assign every order its own dedicated Dasher, start to finish. The cleanest possible model, and every consumer gets the fastest possible delivery for their own order.
Challenges
It is the most expensive way to run a delivery network. At a lunch rush from a dense food hall, you need a separate Dasher for every single order, even when ten of them are going to the same three buildings. You either run out of supply or pay surge to conjure it. Deliveries per Dasher-hour stay low, which pushes up the cost of every delivery and the fees the consumer pays. Simple and correct, and economically unviable at the density where most orders happen.
Approach
When one store has two orders becoming ready within a few minutes of each other, offer both to one Dasher as a single pickup with two dropoffs. Same-store batching is the easy, safe case: the Dasher makes one stop at the counter, grabs both bags, and drives a short route between two nearby drop points. No extra pickup leg, and the prep windows already line up.
Challenges
It only catches orders from the exact same merchant at the same moment, which leaves most of the real opportunity on the table. Two orders from two neighboring restaurants in the same plaza, both heading downtown, are an obvious batch a same-store rule never sees. And even within one store, if the two dropoffs are in opposite directions, the detour can punish the first consumer badly. You need to reason about the route and the timing, not just the store.
Approach
Collect candidate orders and free Dashers in a market over a short window, a couple of minutes, and consider batches across nearby stores whose pickups and dropoffs fall along a compatible route. Pair two orders only when the combined trip is efficient and the detour stays inside a delay budget, a cap on how much later the first order may arrive because of the second. So a batch forms when one Dasher can grab order A, then order B from a store on the way, and drop both with little backtracking. It is rejected when the detour would push either consumer past the cap. The atomic claim from assignment still backstops it, so each order ends up on exactly one Dasher. Batching just means an offer can carry two order ids instead of one.
Challenges
Deciding which orders to pair, in what pickup-and-dropoff order, to minimize total time without blowing the delay budget, is a routing optimization, and over a whole market it is genuinely hard. That solver is the Dasher-assignment lesson's job; DoorDash runs it region-sharded and asynchronously so each market solves its own board in seconds. State the tradeoff plainly. Same-store batching is the simple win you reach for first. Windowed cross-store batching with a delay budget is what lifts efficiency, once you accept a bounded, deliberate delay on one order.
4) How do the three sides stay in sync when each is uncertain?
Zoom out from any one component. The order's fate depends on three parties who each carry their own uncertainty, and the system's job is to keep one coherent story for everyone while none of the three is reliable. The merchant's prep time is a guess. A Dasher's availability and arrival are a guess too. The consumer's ETA rides on both, shown live, wrong the moment either input moves. A senior interviewer pushes on how you hold this together.
The merchant prep time is the root uncertainty, so give it a real answer instead of trusting the number on the tablet. A store that says fifteen minutes for every order is wrong half the time, and during a rush it is wildly optimistic. The better estimate is a prediction built from that store's own history: how long this merchant takes at this hour, for this basket size, under this current load, learned from thousands of past orders. That predicted food-ready time is the input the just-in-time dispatch from deep dive two depends on. It is why DoorDash treats prep-time and ETA as machine-learning problems instead of fields a merchant types in.
Approach
Take the prep estimate the merchant enters when they accept, and feed it straight into dispatch as the food-ready time. It is the merchant's own kitchen, so they ought to know.
Challenges
Merchants are systematically bad at this. They quote an optimistic default that ignores the current queue, so during the exact rush when timing matters most, the estimate is most wrong. Dispatch aims the Dasher at a food-ready time that slips, the Dasher arrives and waits, and the consumer's ETA, built on the same bad number, breaks too. A single wrong input poisons all three sides at once.
Approach
Predict the food-ready time from the store's history and live load, and treat it as a live estimate that gets sharper as the order progresses, not a fixed promise. When the estimate moves, push the change through one channel: update the order, re-evaluate the dispatch timing, and refresh the consumer's ETA, all off the same event. Because the Order service owns the canonical state and emits one event per change, every side reads from the same source and they cannot drift apart. The consumer sees a moving but honest ETA, dispatch retimes the offer if the kitchen slips, and the Dasher is not sent early into a stalled kitchen.
Challenges
Live re-estimation means the consumer's ETA can move, and a number that jumps around erodes trust as much as a wrong one, so you smooth it and avoid yanking it backward. And every party reading off one event stream is only coherent if that stream is the single source of truth. That is why the order service owns state and everyone subscribes instead of holding private copies. The coherence is bought by the architecture, not added on top.
5) How does it scale and survive failure?
The order rate is modest, but a senior interviewer circles to the 6pm-on-Friday questions: how does this hold at the dinner peak, and does it heal when a piece fails without a human. Two facts do most of the work. The marketplace is regional, and the order service owns state behind a replayable log. Each answer leans on a decision already made.
The geography hands us most of the scaling. An order in Chicago is only ever matched to a Chicago Dasher, so the whole system shards by market. Orders, assignments, the dispatch run, and the Dasher geo store all partition by region, and no shard ever carries more than its own city's dinner. DoorDash's real dispatch runs region-sharded and asynchronously for exactly this reason, which turns one impossible global optimization into many independent city-sized ones that each finish in seconds. The dinner peak is also predictable, so capacity is pre-scaled for the evening rush rather than reacting to it.
- The merchant's tablet goes offline. The order is already durable in the Order store and the consumer is already charged, so nothing is lost. The Merchant service retries the push and falls back to a phone confirmation call. If the store cannot be reached at all, the order takes the clean cancel-and-refund path from the
createdstate, before any kitchen work. The order being owned by the order service, not the tablet, is what makes the tablet a recoverable participant instead of a single point of failure. - The dispatch service falls behind at peak. Because dispatch runs asynchronously off the order rather than inside placement, a backlog delays offers but never blocks new orders from being placed and charged. Orders queue for a Dasher, the timing model absorbs a short delay by aiming offers slightly differently, and the system sheds load gracefully instead of failing checkouts. The split between the strict order path and the relaxed dispatch path is what buys this.
- The same order event is delivered twice. Kafka is at-least-once, so redelivery is normal, not exceptional. The idempotency ledger from deep dive one makes a replayed transition a no-op, so a duplicate
deliveredevent does not fire a second payout and a duplicateassigneddoes not double-book a Dasher. Exactly-once behavior is built on at-least-once delivery plus idempotent processing. - A Dasher cancels mid-delivery. Before pickup, the assignment is released, the order drops back to
confirmedorpreparing, and dispatch re-offers it to another nearby Dasher, the same flow run again. After pickup it is rarer and goes to support, since the food is already on the road. Because the assignment is a separate object with its own guarded transitions, releasing it is symmetric and does not disturb the order's kitchen track. - A whole region's order store fails. Promote a synchronous replica within the market; in-flight transactions that had not committed roll back and their callers retry, which for an order means it was never placed and the consumer simply tries again. Partitioning by market keeps the blast radius to one city rather than the country, and the durable event log lets downstream consumers rewind and catch up once the region is back.
What is expected at each level
That is a lot of surface area. Here is how much you need to land, by the level you are interviewing for. DoorDash uses this round to set level, and design is one of the rounds where the debrief moves a candidate up or down a rung.
E4 (mid)
Breadth-led, about 80/20. Reach a working end-to-end flow. An order is written to a transactional store, a ticket goes to the merchant, an asynchronous dispatch step finds a nearby Dasher, and the lifecycle ends in delivered or cancelled. You should recognize the three sides when prompted and know why the order needs strong consistency. The bar: a complete flow you can explain, with the timing and batching nuances found together with the interviewer.
E5 (senior)
Depth now, roughly 60/40, and this is the level the round most often decides. You lead with the three sides unprompted, and reach the just-in-time dispatch reframe on your own instead of reaching for nearest-driver. You model the order as a single-owner state machine with guarded, idempotent transitions. You defend the per-market shard and the order-versus-dispatch consistency split. The bar: a near-optimal order core where you found the dispatch-timing insight and the exactly-once requirement yourself. Most senior candidates are sorted right here, on whether they saw the kitchen as the thing that changes everything.
E6+ (staff)
Depth leads as the scope widens, roughly 40/60. Expect windowed cross-store batching with a delay budget, and prep-time as a learned distribution that re-propagates to all three sides. Expect the outbox and the at-least-once-plus-idempotent story for the event backbone too, plus region-sharded asynchronous dispatch with pre-scaled dinner capacity. You reason about how the marketplace degrades and heals at peak, and you drive. The bar: you run the session and the interviewer redirects rather than rescues, and every consistency and timing choice is deliberate and defended.
Key Takeaways
- ✓Lead with the three sides. A DoorDash order coordinates a consumer, a merchant, and a Dasher, and the merchant's kitchen sitting in the middle is what makes this different from a ride-match. Name the merchant as a first-class actor early.
- ✓The order is the consistency-critical object. One service owns its state, and every transition is a guard plus an idempotency record so it is exactly-once. The canonical state lives in a strongly consistent store partitioned by market.
- ✓Dispatch is a timing problem, not a nearest-driver problem. Predict the food-ready time and offer the delivery so the Dasher arrives just as the food does, instead of idling them at the counter or letting the food go cold.
- ✓Batching is the food-delivery superpower: pair nearby orders over a short window within a delay budget to lift deliveries per Dasher-hour. The marketplace shards cleanly by market, and a Kafka event backbone with idempotent consumers keeps the three sides in sync and surviving the dinner peak.
DoorDash interview guideKnow what DoorDash asks before you walk in.
14 coding · 5 system design · 21 behavioral questions
Get their questions first.
InterviewLegend is independent and not affiliated with or endorsed by any company it covers.