Vibe Engines
YouTube
System Design

Design a Food Delivery System

Step 1 / 9

Learn system design by building a food delivery system like DoorDash, Uber Eats or Deliveroo step by step.

The numbers to beatstatesplaced → deliveredtransitionsparty-triggeredevent logauditable history

The whole design, in writing

Learn system design by building a food delivery system like DoorDash, Uber Eats or Deliveroo step by step. An interactive guide covering the three-sided marketplace, the order lifecycle state machine, the dispatch/matching engine that assigns couriers, real-time location tracking, ML-based ETA prediction, batching, geo-sharding and surge, and the unhappy paths (courier supply, cancellations, restaurant delays).

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What makes food delivery hard?

Three parties who don't know each other — a hungry customer, a busy restaurant, and a moving courier — must be coordinated in real time so hot food gets from kitchen to door. The courier should arrive just as the food is ready, the ETA must be honest, and it all has to work across a city where supply and demand shift by the minute.

Customerorder · track
New in this step: Customer.

A food delivery system is a three-sided marketplace plus a real-time logistics engine. The core pieces: an order lifecycle state machine, a dispatcher that matches couriers to orders optimally, live location tracking, and ETA prediction — all sharded by geography and tuned to the balance of couriers and orders.

What the new pieces do

Customerclient
Places an order, pays, and watches a live map until food arrives. One of three parties in the marketplace.

Step 1 · Three parties, one order

The marketplace shape

Unlike a normal app with users and a backend, here three independent actors must be coordinated per order, each with their own app and their own state. Why does that shape drive everything?

Customerorder · track
The system as it stands at this step.

What makes a delivery app structurally different from a typical two-party app?

  1. Every order threads together three actors who don't know each other, each with their own app and lifecycle. The system's core job is matching (courier↔order) and coordination (customer↔restaurant↔courier) — a logistics problem, not just CRUD.

  2. It's not about screen count. The defining challenge is real-time coordination and matching across three independent parties with shifting supply and demand — a logistics/optimization problem a normal app doesn't have.

  3. Perishability adds timing pressure, but every app has a database. The structural difference is the three-sided marketplace and the real-time dispatch that matches couriers to orders.

It's a three-sided marketplace: each order coordinates a customer, a restaurant, and a courier — three independent apps and lifecycles. The system's real job isn't storing orders; it's matching (which courier for which order) and coordinating the three parties in real time, with the added twist that supply (couriers) and demand (orders) fluctuate constantly. That's a logistics problem.

Why this piece earns its place

Calling this a marketplace has a consequence the diagram hides: we own exactly one of the three sides. Couriers decide when to come online, restaurants decide whether to accept, and neither of them responds to us adding servers. So every decision after this one is made under a supply we can influence but never command, and the thinnest side in any given minute sets the ceiling on the whole product — a city full of idle couriers at 3am is as broken as a Friday dinner rush with none. That makes liquidity, measured per market and per hour, the first thing worth instrumenting, well before dispatch gets clever: a beautiful matching engine with nothing to match is just an elegant queue. It also means the system needs a way to say no — hide a restaurant, quote a longer wait, stop taking orders in a neighbourhood — because an order accepted into a market that cannot serve it is a refund and a lost customer wearing the costume of a sale.

Step 2 · The order is the spine

A lifecycle state machine

An order passes through many stages — placed, paid, accepted, cooking, ready, picked up, en route, delivered — with different parties acting at each. How do you model something that changes hands and state so many times, reliably?

Customerorder · trackOrder Servicelifecycle
New in this step: Order Service.

Model the order as an explicit state machine with well-defined states and allowed transitions (placed → accepted → cooking → ready → picked_up → delivered, plus cancelled/refunded branches). Each transition is triggered by a party (restaurant accepts, courier picks up) and recorded durably, ideally as an event log so the full history is auditable. The order service owns this machine and coordinates everyone against it.

Why this piece earns its place

Every transition in that machine arrives over a mobile network from a party we don’t control, which means it arrives twice, late, and out of order. A courier on a weak connection taps picked up, sees nothing happen, and taps again; a restaurant tablet flushes a queued ready ten minutes after the fact. So a transition can’t be a command that sets a field — it has to name the state it expects to move from, and be rejected as a harmless no-op once the order has left that state. That single rule makes retries free and stops a replayed stale event from dragging a delivered order backwards. The race worth naming out loud is cancellation: if cancel is a flag written beside the state rather than a guarded transition through the same machine, you get an order that is simultaneously cancelled and sitting in a courier’s bag. Cancel competes for the same lock as every other move, and whoever lands first wins — the other party is told, not overruled.

  • statesplaced → delivered
  • transitionsparty-triggered
  • event logauditable history

What the new pieces do

Order Servicegateway
Orchestrates the order: takes payment, drives the state machine, and coordinates restaurant, dispatch and courier.

Step 3 · Bring in the kitchen

The restaurant side

Once paid, the order needs the restaurant to accept and cook it — and the kitchen's behavior (accept/reject, prep time) directly affects when a courier should show up. How does the restaurant fit in?

RestaurantOrder ServiceOrder Store
New in this step: Restaurant, Order Store. · swipe to pan the diagram

Send the order to the restaurant (via their tablet/app/POS integration) to accept or reject, then track it through cooking → ready. Capture the restaurant's prep time (estimated and actual) — it's a first-class signal: dispatch uses it to time the courier's arrival, and the ETA model learns from it. Handle rejections/86'd items by refunding or re-routing. The kitchen is a party whose timing the whole system must model, not just a passive step.

Why this piece earns its place

The uncomfortable part is that prep time, the signal everything downstream leans on, is reported by the party with the strongest reason to shade it. A kitchen that notices a courier turns up sooner once it taps ready will tap while the order is still being bagged; one judged on lateness will pad its estimate instead. Either way we end up training on a button rather than on food. The tempting correction is to learn from accept to pickup, since we witness both ends — but pickup also contains however long we took to get a courier to that counter, and that delay is produced by a dispatch decision this very prediction drives. Train on it and the model learns our own lateness, then feeds it straight back into the choice that caused it. So bracket the measurement with the courier instead: the moment they are standing at the counter, and the moment the bag is handed over. On the orders where the courier was already waiting when the food came up, accept-to-pickup is a clean prep label too; where they arrived after it was up, the gap is ours rather than the kitchen’s and the order is dropped from training. That same restriction is what makes the tap worth comparing at all — a kitchen whose ready consistently leads a handoff a waiting courier was there for is one the dispatcher should discount.

What the new pieces do

Restaurantclient
Accepts (or rejects) the order, cooks it, and marks it ready. Its prep time is a key input to timing and dispatch.
Order Storedata
Durable record of each order and its state (placed → accepted → cooking → ready → picked → delivered).

Step 4 · The heart: matching

The dispatch engine

Now the hard part. An order is cooking; somewhere in the city are hundreds of couriers in various states. Which courier gets this order — and when do you even assign it? Get this wrong and food is cold, couriers idle, or customers wait forever.

CustomerRestaurantCourierOrder ServiceDispatcherOrder Store
New in this step: Courier, Dispatcher. · swipe to pan the diagram

An order needs a courier from hundreds in the city. What's the right matching?

  1. Nearest-and-immediate is naive: assign too early and the courier waits at the restaurant while food cooks (wasted); "nearest" ignores who's about to free up, traffic, and batching opportunities. Good dispatch optimizes globally over time, not greedily per order.

  2. Free-for-all grabbing leads to poor global outcomes — cherry-picking, unbalanced load, bad ETAs. A central optimizer assigns to maximize overall efficiency and fairness.

  3. The dispatcher solves an ongoing assignment/optimization problem: match couriers to orders to minimize total delivery time and courier idle time, timing the assignment to the food being ready, and batching nearby orders. It weighs who'll be free soon, traffic, and prep time — far beyond "nearest free courier".

Build a dispatch engine that treats assignment as an ongoing optimization, not a greedy nearest-match. It decides which courier and when to assign, weighing: predicted ETA and travel time, restaurant prep time (assign so the courier arrives as food is ready), courier utilization and who's about to free up, and batching nearby orders onto one trip. It's a continuous matching problem over a live pool of couriers and orders — the algorithmic core of the whole system.

Why this piece earns its place

The knob this step introduces is how long dispatch waits before committing an order to a courier, and it cuts both ways. Hold the order and the solve gets better — more couriers come free, a batch partner shows up, the arrival lands closer to food-ready. Hold it too long and nobody in range can reach the kitchen before the bag is up, so the food waits, and because the optimizer runs continuously that slip propagates into every order queued behind it. The dark side of re-solving every few seconds is churn: an assignment that keeps improving on paper can be taken back from a courier who has already started driving. That costs real miles, and it costs trust, which is the one input we can’t buy back quickly. So the rolling solve should treat unassigned orders as freely movable and accepted ones as very nearly frozen — reassigning only when the gain is large, or when the courier has plainly stopped moving toward the restaurant.

  • who + whennot nearest-now
  • batchnearby orders → one trip
  • optimizeETA + prep + utilization

What the new pieces do

Courierclient
The driver/rider. Gets assigned an order, picks it up, and delivers it — streaming GPS the whole way.
Dispatcherservice
The matching engine: assigns the right courier to each order, optimizing ETA, batching and courier utilization.

Step 5 · Where is my courier?

Real-time location tracking

Dispatch needs to know which couriers are near a restaurant, and the customer wants a live dot moving toward them. That means ingesting GPS from thousands of couriers and answering "who's nearby?" and "where is order X's courier?" continuously.

CustomerRestaurantCourierOrder ServiceDispatcherOrder StoreLocation
New in this step: Location. · swipe to pan the diagram

Run a location service that ingests courier GPS pings (every few seconds) and maintains a geo-spatial index (geohash/quadtree) so dispatch can query "couriers within N minutes of this restaurant" cheaply. Push each order's courier position to the customer's app over WebSockets for the live map. Keep location updates on a high-throughput path (they're frequent and lossy-tolerant — a missed ping is fine) separate from the transactional order data.

Why this piece earns its place

Both consumers of this feed are being handed something far noisier than it looks. A raw GPS trace in a dense city snaps between streets, jumps backwards, and goes quiet in a tunnel or whenever the phone decides to sleep the app — so a customer’s live dot, rendered faithfully, shows a courier driving the wrong way down a road they were never on. The map should therefore be a rendering, not a feed: snap positions to the road network, animate along the predicted route between pings, and when the pings stop, show the last known position with its age rather than a confident frozen dot. Dispatch needs the opposite correction. Its question is never "who is within a kilometre" but "who is within a few minutes" — straight-line proximity cheerfully picks the courier across the river, one bridge and a long left turn away, over the one four streets back. Index by position, rank by travel time.

  • GPSevery few seconds
  • geo-indexnearest-courier queries
  • WebSocketlive map to customer

What the new pieces do

Locationservice
Ingests courier GPS, maintains a geo-index of nearby couriers for dispatch, and powers the live tracking map.

Step 6 · Honest timing

ETA prediction

"Arrives in 35 min" is a promise that shapes the whole experience — and it's the sum of several uncertain parts: how long the kitchen takes, how long until a courier is free and drives there, pickup wait, and drive to the customer. How do you predict it well?

Dispatchermatch courierETA Modelpredict time
New in this step: ETA Model.

Use an ML model to predict ETA as the sum of its parts: restaurant prep time (learned per restaurant/item/load), courier assignment + travel time (real-time traffic and distance), pickup wait, and drop-off travel. Feed it live signals (current courier positions, restaurant backlog, weather, time of day) and learn from actual delivery times to keep improving. The prediction feeds the customer quote and the dispatcher (when to assign).

Why this piece earns its place

An ETA isn’t really a prediction, because we publish it and it becomes a promise. That makes the error asymmetric in a way a plain accuracy score can’t see: arriving early is close to free, arriving late is the complaint, the refund and the churn. So the model should be trained and judged with an asymmetric loss and reported on its tail — the share of deliveries that blow past the quoted time — rather than on average error, which a model that is wildly early half the time and wildly late the rest can post a lovely number on. It also means the figure we show and the figure we optimize against are allowed to differ: quote the customer with some cushion, dispatch against the unpadded estimate. That cushion is its own dial, because padding it until we are never late is the same move as pushing the customer to order from somewhere that promises sooner.

  • prep + travel + waitsummed
  • live signalstraffic, backlog, weather
  • learnfrom actual times

What the new pieces do

ETA Modelservice
Predicts prep + travel + wait times so quotes are honest and dispatch times the courier to arrive as food is ready.

Step 7 · A city at a time

Geo-sharding, batching & surge

A national service isn't one giant matching problem — dispatch in New York has nothing to do with couriers in LA. And within a city, demand spikes (Friday dinner) while courier supply is finite. How do you scale and balance it?

CustomerRestaurantCourierOrder ServiceDispatcherOrder StoreLocationETA Model
The system as it stands at this step. · swipe to pan the diagram

The service runs in 200 cities, and Friday dinner rush floods one of them with orders. What is the architecture and the fix?

  1. New York couriers can never serve Los Angeles orders, so solving them jointly is pure wasted compute on a problem that has zero cross-city interaction — it doesn't scale and doesn't need to.

  2. Each market is a self-contained, tractable optimization near its own data. Within a flooded city, surge pay pulls more couriers online and gently cools demand — a live pricing loop that rebalances the one market that is actually stressed.

  3. A blunt national cap punishes cities that are perfectly balanced to fix a problem local to one city. The imbalance is local; the fix should be too.

Geo-shard the system by city/region so dispatch, location and matching run per market — each a self-contained, tractable optimization near its data (low latency, natural partition). Batch orders heading the same way onto one courier trip to raise efficiency. And balance supply vs demand with surge/incentives: when orders outnumber couriers, raise courier pay (and possibly customer fees) to pull more couriers online and gently smooth demand — a live marketplace-pricing loop per region.

Why this piece earns its place

Sharding by city bakes in a claim that is true nationally and false at the seams. A boundary drawn on a municipal line runs straight through neighbourhoods where the best courier for this restaurant is two blocks away and in the other shard — and since boundaries tend to follow the dense parts, the bad matches cluster exactly where the volume is. So draw markets from where deliveries actually happen rather than from an administrative map, and let candidate pools overlap at the edges so a courier near a border is visible to both sides. Overlap then creates the failure it was always going to: two shards offering the same courier a different order in the same second. However the map is cut, a courier has exactly one owner deciding their next job, and an assignment has to be confirmed against that owner before it is communicated to anyone. Shards also aren’t equal — one metro can carry more load than several others combined, so expect to split the hot one rather than assuming a city is a unit.

  • geo-sharddispatch per market
  • batchshared-route orders
  • surgebalance supply/demand

Step 8 · The sharp edges

Supply, cancels & cold food

Reality is messy: not enough couriers at peak, restaurants that reject or run slow, customers who cancel mid-cook, and the constant risk of food arriving cold or late.

CustomerRestaurantCourierOrder ServiceDispatcherOrder StoreLocationETA Model
The system as it stands at this step. · swipe to pan the diagram

Handle courier shortage with surge incentives, batching, and honest longer ETAs (or pausing a restaurant) rather than accepting orders you can't deliver. Manage cancellations through the state machine with clear refund rules depending on stage (free before cooking, partial after). Absorb restaurant delays by learning real prep times and re-timing or re-assigning couriers. Guard reliability with idempotent payments, timeouts and reassignment if a courier goes unresponsive, and monitor food-cold risk (dispatch-to-ready mismatch) as a core quality metric.

Why this piece earns its place

Worth saying out loud in an interview: none of these are refunds, they are three-way settlements. A cancel after cooking has started means the customer’s money goes back, but the restaurant is standing over food it has already paid for and the courier may be halfway there. Refunding the customer and stopping isn’t neutrality — it quietly pushes the cost of our worst minutes onto the two sides whose supply we depend on and cannot conjure. So every terminal state carries a payout rule alongside its refund rule, and both are decided by the state the order was in when it went wrong, which is exactly what makes the recorded transitions earlier worth their trouble: the settlement is derived from the history rather than argued about afterwards. The cheap instinct is to make the customer whole and sort the other two out case by case in a support queue. That is the most expensive version of this, because it is manual at precisely the moments when volume spikes.

You did it

You just designed a food delivery system.

CustomerRestaurantCourierOrder ServiceDispatcherOrder StoreLocationETA Model
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • It's a three-sided marketplace (customer/restaurant/courier) plus a real-time logistics engine.
  • The order is a durable state-machine spine — placed → accepted → cooking → ready → picked → delivered.
  • The restaurant's prep time is a first-class signal for timing, not a passive step.
  • The dispatcher is the heart: an ongoing optimization of who and when, weighing ETA, prep, batching, utilization — not nearest-now.
  • A location service (geo-index + GPS streaming) powers nearest-courier queries and the live map.
  • ETA is an ML sum of prep + wait + travel, feeding both the customer quote and dispatch timing.
  • Geo-shard by city, batch shared-route orders, and use surge pricing to balance supply and demand.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why is dispatch modeled as an ongoing optimization instead of assigning the nearest free courier?

    The naive "assign the geographically nearest available courier the moment the order is placed" fails on several fronts that only show up at marketplace scale. Timing: if you assign immediately, the courier often arrives while the food is still cooking and waits idle at the restaurant, wasting the most expensive resource in the system; you want to assign so the courier arrives just as the food is ready, which depends on the restaurant's prep time. Global efficiency: "nearest right now" ignores couriers who are about to free up a block away, ignores traffic, and misses batching opportunities where two orders heading the same direction could ride together. Fairness and utilization: greedy per-order assignment can strand some couriers and overload others. So real dispatch treats the whole city as a rolling assignment problem — typically a bipartite matching between the live pool of orders and couriers solved every few seconds — that minimizes total delivery time and idle miles across all orders at once, decides not just which courier but when to commit the assignment, and factors in predicted ETAs, prep times, and batching. It's the algorithmic core of the product precisely because small improvements compound across millions of deliveries.

  2. Why model the order as a state machine, and why emit events?

    A delivery order is a long-running, multi-party process that changes hands and state many times — placed, paid, accepted or rejected by the restaurant, cooking, ready, picked up, en route, delivered, with cancellation and refund branches — and different independent actors trigger different transitions. Modeling it as an explicit state machine makes this reliable: you enumerate the valid states and the allowed transitions between them, so illegal moves (delivering an order that was never picked up) are impossible, and every transition is recorded durably so that if a service restarts it resumes from the persisted state rather than losing track of an in-flight order. Emitting an event for each transition (an event log) adds two big benefits: it gives you a complete, auditable history of what happened to every order (essential for support, refunds, and analytics), and it lets all the other subsystems — live tracking, push notifications, dispatch, analytics — react to state changes by subscribing to events instead of constantly polling the order database. The state machine is the reliable spine that coordinates three parties through a process that can take an hour and must never get confused about where an order stands.

  3. How does location data flow differently from order data, and why keep them separate?

    They have opposite characteristics and requirements, so mixing them would hurt both. Order data is transactional and must be correct and durable: payment, state transitions, and refunds need ACID guarantees and can never be silently lost, but they change relatively infrequently over an order's lifetime. Location data is the opposite: thousands of couriers each emit a GPS ping every few seconds, so it's extremely high-volume, but it's also tolerant of loss (a single missed ping is irrelevant since another arrives in seconds) and only the latest position really matters. So location flows on its own high-throughput, lossy-tolerant path: pings feed a geo-spatial index (geohash or quadtree) that answers "which couriers are within N minutes of this restaurant?" for dispatch, and a stream that fans each order's courier position out to the customer's app over WebSockets for the live map. Routing that firehose through the transactional order database would overwhelm it and provide no benefit, since you don't need durability or transactions for a position that's obsolete in seconds. Keeping them separate lets the order store stay consistent and the location pipeline stay fast.

  4. Why is ETA treated as a sum of separate predictions rather than one number?

    Because the total time to delivery is genuinely composed of distinct phases, each governed by different factors and uncertainty, and predicting them separately is both more accurate and more useful. The phases are roughly: how long the restaurant takes to prepare the food (which depends on the specific restaurant, the items, and current kitchen load), how long until a courier is assigned and travels to the restaurant (which depends on courier supply, positions, and traffic), how long the courier waits at pickup, and how long the drive to the customer takes (distance and traffic). Modeling each with its own signals — learned per-restaurant prep times, live courier positions, real-time traffic, weather, time of day — and summing them yields a far better estimate than trying to regress the whole thing as one opaque number, and it degrades gracefully because you can update individual components as new information arrives. Crucially, the decomposition is used in two places: the sum sets an honest expectation for the customer's quote, and the components feed dispatch so it can time the courier's assignment to the predicted food-ready moment. The system then compares predictions to actual delivered times and retrains, closing the loop so estimates keep improving.

  5. Why geo-shard the system, and how does surge pricing fit in?

    Food delivery is inherently local: an order in New York can only be served by couriers in New York, and there's no interaction whatsoever between the New York and Los Angeles matching problems. That means the natural way to scale is to partition the system by city or region so that dispatch, the location index, and matching each run per market — a self-contained, tractable optimization operating near its own data with low latency, and you scale nationally simply by adding markets rather than trying to solve one impossibly large global assignment. Within each local market, the fundamental challenge is that supply (couriers online) and demand (orders) fluctuate independently and often mismatch — a Friday dinner rush can flood a city with orders while courier supply lags — which would blow out ETAs or leave orders unassignable. Surge pricing is the marketplace's real-time control loop for this: when demand outstrips supply, raising courier pay (and sometimes customer fees) pulls more couriers online and slightly dampens demand, nudging the local market back toward balance so ETAs stay reasonable. Price is essentially the dial that couples supply to demand per region, continuously, without central planning.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. A food delivery app is structurally a…

    Every order matches and coordinates customer, restaurant and courier in real time.

  2. The order is best modeled as…

    Valid transitions + persisted state + events make the multi-party process reliable and reactive.

  3. Good dispatch is…

    Greedy nearest-now strands couriers and ignores batching and prep timing.

  4. Courier GPS data flows separately from order data because…

    Frequent pings feed a geo-index + live map on a fast path, not the ACID order store.

  5. The system scales and balances by…

    Delivery is local: partition by market; price balances courier supply against demand.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Order: a customer places and pays for an order, then watches it to the door.
  • Cook it: the restaurant accepts (or rejects) and drives it cooking → ready.
  • Dispatch: match the right courier to each order — which one and when to assign.
  • Track: stream the courier’s live position to the customer’s map.
  • Quote an ETA: an honest "arrives in 35 min" the customer and the dispatcher can trust.

The qualities that shape everything

Each one names the mechanism that buys it.

Never lose track of an in-flight order
Model the order as a durable state machine — only valid transitions are allowed and every change is persisted, so a restart resumes from saved state instead of losing an order mid-cook.
Hot food, busy couriers
A dispatch engine that treats assignment as a rolling optimization — which courier and when — timing arrival to food-ready and batching nearby orders, not grabbing the nearest free one now.
Answer "who’s nearby?" and "where’s my courier?" continuously
A location service ingests GPS on a high-throughput, loss-tolerant path into a geo-index for dispatch and a WebSocket stream for the customer’s live map — off the transactional order store.
An honest arrival promise
Predict ETA as a sum of parts — prep + wait-for-courier + travel + handoff — each learned from live signals and corrected against actual delivery times.
Scale to hundreds of cities
Geo-shard dispatch, location and matching per market — each a self-contained optimization near its data, since a New York order can never use an LA courier.
Stay good enough when the market is out of balance
Surge incentives and honest longer ETAs for courier shortage, staged refunds through the state machine for cancels, and reassignment when a courier goes unresponsive.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

A state machine + event log over a single mutable order row

An hour-long, multi-party process mutated in place loses its history and allows illegal jumps. Explicit states with recorded transitions make only valid moves possible, survive restarts, and let tracking, notifications and dispatch react to events instead of polling.

Rolling assignment optimization over nearest free courier, now

Assigning the nearest courier the instant an order drops strands them idle at the restaurant while the food cooks, and ignores batching and who’s about to free up. A city-wide matching solved every few seconds minimizes total time and idle miles.

Location on its own fast path over GPS through the order database

Thousands of pings a second are high-volume but loss-tolerant — routing that firehose through the ACID order store would overwhelm it for data obsolete in seconds. Durable orders and lossy positions get separate pipelines.

ETA as a sum of parts over one opaque regressed number

Delivery time is genuinely prep + wait + travel + handoff, each with its own signals. Modeling and summing the phases is more accurate, degrades gracefully, and the components feed dispatch’s timing — a single number can’t.

Geo-shard + local surge over one national dispatch optimizer

New York couriers can never serve LA orders, so a joint national solve is wasted compute on a problem with zero cross-city interaction. Partition by market and let surge rebalance only the city that’s actually stressed.

The answer, out loud

What a strong answer to “Design a Food Delivery System” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–4 min

    Frame what this actually is

    Let me frame it before I draw anything. This isn’t a shopping app with delivery bolted on — every order brings together three parties who don’t know each other and don’t work for us: a hungry customer, a busy kitchen, and a courier moving around the city. Two of those three choose whether to show up at all. So it is a matching problem with a real-time logistics problem on top. The core I’d commit to is: take and pay for an order, get it cooked, put the right courier on it at the right moment, show the customer where it is, and quote an arrival time we can stand behind.

  2. 4–9 min

    The order is the spine

    I’d start with the order itself, because everything else hangs off it. It lives for an hour, changes hands three times, and different people push it forward at each stage, so I’d model it explicitly as a state machine — placed, accepted, cooking, ready, picked up, delivered, with cancel and refund branches — and persist every transition rather than mutating one row in place. Two things fall out of that. A service can crash, come back, and still know where every in-flight order stood. And because each transition is an event, tracking, notifications and dispatch can all react to it instead of polling the order database.

    Built in step 2: A lifecycle state machine
  3. 9–14 min

    Bring in the kitchen

    Then I’d bring the restaurant in, because it isn’t a passive step, it’s a party with its own timing. It accepts or rejects, it cooks, it marks the order ready, and how long that takes is what decides when a courier should be standing at the counter. So prep time is a first-class learned signal, per restaurant and item and current load, not one global guess. Wrong in one direction leaves a courier idle in the shop; wrong in the other leaves food on a warming shelf going cold. Neither of those is recoverable later in the journey.

    Built in step 3: The restaurant side
  4. 14–22 min

    Dispatch is the product

    Dispatch is where I’d spend most of the time, because it is the actual product. The tempting answer is to grab the nearest free courier the moment the order comes in, and I’d argue against it: that strands somebody at the counter while the food is still cooking, ignores whoever is about to finish a drop one street away, and throws away every chance to put two orders heading the same direction onto one trip. What I’d build instead is a rolling optimization across the whole city — the live pool of orders against the live pool of couriers, re-solved continuously — deciding not just which courier but when to commit, aiming their arrival at the moment the food is ready.

    Built in step 4: The dispatch engine
  5. 22–28 min

    Where everyone is

    Dispatch can’t do any of that without knowing where people are, so next is a location service. Couriers stream GPS every few seconds, feeding two different consumers. Dispatch needs spatial queries — who could be at this restaurant soon — so the pings maintain a geo-index. The customer needs the live dot, which is a stream pushed down a WebSocket. I’d keep all of this well away from the order database. Positions are huge in volume and disposable — a ping we drop is replaced two seconds later — whereas an order transition can never be lost. Different guarantees, so different pipelines.

    Built in step 5: Real-time location tracking
  6. 28–34 min

    An arrival time we can defend

    The ETA I’d model as a sum rather than one number, because it genuinely is one: how long the kitchen takes, how long until a courier is free and gets there, how long they wait at pickup, and the drive to the door. Each part has its own signals — traffic, kitchen backlog, weather, time of day — and predicting them separately is more accurate and more useful, because those pieces are what dispatch needs to time the assignment. Then I’d close the loop against actual delivery times, since every order eventually tells us what the right answer was.

    Built in step 6: ETA prediction
  7. 34–40 min

    Scaling is geography

    On scale, I’d push back on treating this as one enormous matching problem. A courier in New York can never take an order in Los Angeles, so there is no interaction to solve for and the natural partition is geography. I’d shard dispatch, the location index and matching per market, so each is a bounded problem near its own data, and we grow nationally by adding markets rather than making one solver bigger. Within a market, when Friday night floods it, surge is the control loop: paying couriers more pulls supply online and cools demand a little, touching only the city that is actually stressed.

    Built in step 7: Geo-sharding, batching & surge
  8. 40–45 min

    The messy half, and what I’d do next

    The last thing I’d cover is the unhappy path, because that’s where this system really lives. Not enough couriers, a restaurant that rejects, a customer who cancels mid-cook, a courier whose phone goes dark. My rule would be to degrade honestly rather than pretend — quote a longer arrival, pause a restaurant we can’t serve, reassign rather than hope. The trade-off I’d name is that nearly everything here buys a better delivery at the cost of a more complicated promise — holding an order back for a better match, batching two drops, padding an estimate. With more time I’d go deeper on batching — specifically how much detour I’m willing to add to the first customer’s food in order to pick up the second — and on how this behaves when a market is short of couriers for hours rather than minutes.

    Built in step 8: Supply, cancels & cold food

What this teaches

Learn system design by building a food delivery system like DoorDash, Uber Eats or Deliveroo step by step. An interactive guide covering the three-sided marketplace, the order lifecycle state machine, the dispatch/matching engine that assigns couriers, real-time location tracking, ML-based ETA prediction, batching, geo-sharding and surge, and the unhappy paths (courier supply, cancellations, restaurant delays).

Key takeaways

  • It's a three-sided marketplace (customer/restaurant/courier) plus a real-time logistics engine.
  • The order is a durable state-machine spine — placed → accepted → cooking → ready → picked → delivered.
  • The restaurant's prep time is a first-class signal for timing, not a passive step.
  • The dispatcher is the heart: an ongoing optimization of who and when, weighing ETA, prep, batching, utilization — not nearest-now.
  • A location service (geo-index + GPS streaming) powers nearest-courier queries and the live map.
  • ETA is an ML sum of prep + wait + travel, feeding both the customer quote and dispatch timing.
  • Geo-shard by city, batch shared-route orders, and use surge pricing to balance supply and demand.

Concepts covered

  • What makes food delivery hard?
  • The marketplace shape
  • A lifecycle state machine
  • The restaurant side
  • The dispatch engine
  • Real-time location tracking
  • ETA prediction
  • Geo-sharding, batching & surge
  • Supply, cancels & cold food
built to match a hungry customer, a busy kitchen, and a moving courier in real time — make the calls, kill dispatch, run the gauntlet.
Finished this one? 0 / 65 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs