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

In the interview room

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.

Functional requirements

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.

Non-functional requirements

The qualities that shape the whole design — 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 logover 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 optimizationover 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 pathover 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 partsover 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 surgeover 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.

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

Design a Food Delivery System — read the full walkthrough as text

the same steps, decisions & trade-offs, for reading, reference & search

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.

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.

How to read this: Each step opens with a real design decision — make the call before I show you what ships. Watch the marketplace grow, and at the end kill the dispatcher and kill tracking to see the critical path vs the experience layer. Hit Begin.

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?

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

The call: It just has more screens. — 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.

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.

Marketplace + logistics: Two systems in one: a marketplace (match supply and demand — couriers to orders) and a real-time logistics layer (track, route, time the handoff). Everything downstream — dispatch, ETA, tracking, surge — serves one of those two jobs.

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?

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.

State machine + events: A durable state machine makes a multi-party, long-running process reliable: only valid transitions are allowed, every change is recorded, and if a service restarts it resumes from the persisted state. Emitting an event per transition lets tracking, notifications, analytics and dispatch all react without polling.

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?

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.

Prep time is a signal: The restaurant isn't a black box — its accept latency and prep duration drive courier timing. Learn each restaurant's real prep time per item and load, so you dispatch a courier to arrive as the food is bagged, not 15 minutes early (courier idle) or late (food cold).

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.

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

The call: A dispatch engine that optimizes assignment over time — considering ETA, prep time, batching and courier flow, not just nearest-now. — 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.

Assignment, not just proximity: Naive "nearest free courier now" fails: it strands couriers waiting on slow kitchens and ignores batching and near-future supply. Real dispatch is a rolling optimization (often a bipartite matching / assignment problem solved every few seconds) minimizing total time and idle miles across all orders and couriers at once.

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.

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.

Geo-index + streaming: Two needs: spatial queries for dispatch (nearest couriers → a geo-index) and live tracking for customers (a stream of positions → WebSocket fan-out). GPS is high-volume and tolerant of loss, so it flows on its own fast path, not through the order database.

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?

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).

ETA as a sum of predictions: ETA isn't one number — it's a pipeline of predictions (prep + wait-for-courier + travel + handoff) each with its own model/signals. It's used twice: to set an honest customer expectation and to time dispatch so the courier and the food are ready together. Close the loop with actuals.

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?

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

The call: Geo-shard dispatch per city/region, and use surge pricing as the local supply/demand control loop. — 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.

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.

Local markets + pricing: Delivery is inherently local, so partition by geography — it bounds each dispatch problem and scales horizontally by adding markets. Surge pricing is the marketplace's control loop: price is the dial that pulls courier supply toward demand in real time, keeping ETAs reasonable when a city gets hungry at once.

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.

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.

Design for the unhappy path: Too few couriers → surge + batch + honest ETA. Cancel → staged refunds via the state machine. Slow kitchen → learn prep, re-time dispatch. Courier drops → reassign. The marketplace is always slightly out of balance; the system's job is to keep it good enough in real time, not perfect.

You did it

You just designed a food delivery system.

  • 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.
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