System Design · step by stepDesign a Food Delivery System
Step 1 / 9

Design a Food Delivery System — the walkthrough in full

A written version of the interactive walkthrough above — the same steps, decisions and trade-offs, laid out for reading, reference and 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?

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.

  • I — t
  • T — h
  • T — h
  • T — h
  • A
  • E — T
  • G — e
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 / 50 System Designs done

Explore the topic

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