System Design (High-Level Design) Module 25 – Case studies: location, mobility and logistics
Design food delivery like Swiggy
Design food delivery like Swiggy or Zomato: one order across customer, restaurant and rider, then assignment, batching, ETAs and dinner and rain peaks.
What you will learn
- Estimate orders, rider location reports, riders needed and storage for a food delivery platform
- Implement one order state machine shared by the customer, the restaurant and the rider, with a timer and refunds
- Compare dispatch policies and batching by measured delivery times and rider time
- Size riders for lunch, dinner and rain peaks with Little's law, and shrink delivery areas when needed
- Plan ETAs, failure drills and growth to 10× and 100× for the order and dispatch paths
Before you start
What you’ll learn · Before you start
What you will learn
- Estimate orders, rider location reports, riders needed and storage for a food delivery platform
- Implement one order state machine shared by the customer, the restaurant and the rider, with a timer and refunds
- Compare dispatch policies and batching by measured delivery times and rider time
- Size riders for lunch, dinner and rain peaks with Little's law, and shrink delivery areas when needed
- Plan ETAs, failure drills and growth to 10× and 100× for the order and dispatch paths
Before you start
On this page
A food delivery platform takes an order from a customer, gets a restaurant to cook it and a rider to bring it, and keeps all three informed until the food arrives, for 4 million orders a day. The heart of the design is one order state machine that the three parties move forward: placed, accepted by the restaurant within 3 minutes, cooking, ready, collected and delivered, with cancellations and refunds at the points where they are allowed.
Around it sit browsing, which shows only restaurants that can deliver to the customer, dispatch, which chooses a rider so that they reach the restaurant as the food is ready and sometimes gives one rider two orders, and peak handling, because lunch, dinner and rain bring the most orders exactly when riders are scarce and roads are slow.
Not affiliated
Based on public information about Swiggy and Zomato. MySmartCoPilot is not affiliated with Swiggy and Zomato, and Swiggy and Zomato’s real architecture may differ from what this lesson describes.
Three parties move one order, and each sees the same state
Ask these first, and note the answer you assume:
- What is delivered? Cooked food from restaurants. Groceries from a platform’s own stores are a different design.
- How big? 4 million orders a day nationally, 15 % of them in the busiest dinner hour, and 300,000 riders online at that peak.
- Who takes part? The customer, the restaurant with a tablet or app, and the rider with a phone.
- When is payment taken? When the order is placed, so refunds are the compensations of the flow.
- What matters most? Food that arrives hot and when promised, restaurants that are never overloaded, and riders who are not left waiting.
So placing and tracking stay available in the busiest hour, every party sees the same state of an order, and no failed order keeps its payment.
The customer’s app lists restaurants near a point and reads their menus; POST /v1/orders takes the items, the
address and an Idempotency-Key. The restaurant’s and the rider’s apps keep a persistent connection for new orders,
job offers and their replies.
| Data | Key | Holds |
|---|---|---|
| Order | order id | State, items, prices, payment, refunds, timestamps of every state |
| Restaurant | restaurant id | Location, delivery area as a polygon, hours, a preparation-time estimate |
| Menu | restaurant id | Items, prices and availability, cached per area |
| Rider | rider id | State and position, kept in memory as in ride matching |
| Assignment | assignment id | The rider, one or two orders, the planned stops |
Checkpoint
Check the scope
About 167 orders a second need some 294,000 riders at dinner
| Quantity | Working | Result |
|---|---|---|
| Orders, busiest hour | 4 million × 15 % ÷ 3,600 s | about 167 a second |
| Rider location reports | 300,000 riders, one every 5 s | 60,000 a second |
| Riders needed at the peak | 10,000 orders a minute × 25 minutes ÷ 85 % busy | about 294,000 |
| Order storage | 10 KB × 4 million × 365 × 3 copies | about 44 TB a year |
| Menu and restaurant reads | 20 views per order, 15 % in the busiest hour | about 3,300 a second |
The third row is Little's law: In a stable system, the average number of requests inside equals the arrival rate times the average time each one spends inside, L = λW. At 100 requests a second and 50 ms each, about 5 are in progress. More in “Little's law, utilisation and server counts”: riders busy at once equal orders a minute times the minutes each order keeps a rider busy. It explains why the platform needs about as many riders online at the peak as it has, and why anything that lengthens a delivery, such as rain, needs riders that do not exist.
Exercise · Easy · Python
Size orders, rider updates, riders and storage for food delivery
Write four functions in sizing.py for the estimate sheet of a food delivery platform. An hour has 3,600 seconds, a year 365 days, and 1 TB is 10¹² bytes.
orders_per_second_at_peak(orders_per_day, busiest_hour_share): orders a second in the busiest hour, when that hour carriesbusiest_hour_shareof the day's orders (0.15 for 15 %).rider_updates_per_second(riders_online, report_every_s): location reports a second from the riders online.riders_needed(orders_per_minute, minutes_per_order, busy_share): riders needed by Little's law, when each order keeps a rider busy forminutes_per_orderminutes and riders are planned to be busy at mostbusy_shareof the time.order_storage_tb_per_year(orders_per_day, bytes_per_order, copies): the storage a year of orders needs, kept incopiescopies.
The sample tests accept each figure within 10 %, as an estimate should.
Starter code · sizing.py
HOUR_SECONDS = 3_600
def orders_per_second_at_peak(orders_per_day, busiest_hour_share):
"""Orders a second in the busiest hour."""
# Replace this line with your code.
return 0
def rider_updates_per_second(riders_online, report_every_s):
"""Location reports a second."""
# Replace this line with your code.
return 0
def riders_needed(orders_per_minute, minutes_per_order, busy_share):
"""Riders needed by Little's law, with headroom."""
# Replace this line with your code.
return 0
def order_storage_tb_per_year(orders_per_day, bytes_per_order, copies):
"""TB a year of orders, in `copies` copies."""
# Replace this line with your code.
return 0 The sample tests · test_sizing.py
import math
from sizing import order_storage_tb_per_year, orders_per_second_at_peak, rider_updates_per_second, riders_needed
def near(actual, expected):
"""Within 10%, as an estimate should be."""
return math.isclose(actual, expected, rel_tol=0.10)
def test_orders_at_peak():
"""the busiest hour's share of the day, per second"""
assert near(orders_per_second_at_peak(4_000_000, 0.15), 167)
assert near(orders_per_second_at_peak(300_000, 0.2), 16.7)
def test_rider_updates():
"""one report per rider per interval"""
assert near(rider_updates_per_second(300_000, 5), 60_000)
assert near(rider_updates_per_second(20_000, 10), 2_000)
def test_riders_needed():
"""Little's law, then headroom"""
assert near(riders_needed(8, 30, 0.85), 282)
assert near(riders_needed(10_000, 25, 0.85), 294_118)
def test_order_storage():
"""a year of orders in every copy"""
assert near(order_storage_tb_per_year(4_000_000, 10_000, 3), 43.8)
assert near(order_storage_tb_per_year(300_000, 8_000, 2), 1.75) A hint
The busiest hour's orders are orders_per_day * busiest_hour_share; divide by 3,600 for a second. Little's law says the riders busy at once are the arrival rate times the time each order takes; divide by busy_share to keep headroom. Storage multiplies orders, bytes, 365 and copies, then divides by 10**12.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Checkpoint
Check the estimate
One order moves through browsing, the kitchen and a rider
The first complete design of food delivery
Text description of the diagram
The diagram shows the parts of the design from top to bottom.
- The customer app calls the API gateway. Browsing reads restaurants, menus and delivery areas, cached per area.
- Placing an order goes to the order service: a state machine that also takes the payment, issues refunds and writes an outbox row for every change.
- The order service tells dispatch when the food is expected to be ready; dispatch assigns a rider, possibly with a second order, from rider locations held in memory.
- Order updates and dispatch's offers travel through the connection gateways: the restaurant app accepts orders and marks them ready, and the rider app receives offers and confirms pickup and delivery.
Browsing reads restaurants, menus and delivery areas from caches per area; menus are searchable by dish, which needs text analysis that splits and normalises words so that “Paneer” also finds “paneer” (Elastic). Placing the order creates it in the order service, which takes the payment and sends the order to the restaurant’s app through the connection gateways. Accepting starts a timer: the restaurant must accept within 3 minutes or the order is cancelled and refunded. Dispatch learns when the food should be ready, chooses a rider, possibly with a second order, and offers the job through the same gateways. Pickup and delivery are confirmed by the rider’s app, while the customer follows the rider’s position. Every change writes an outbox row, so notifications and ETAs follow from the same events. Follow one order:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- Browse. The customer's app reads restaurants, menus and delivery areas from caches kept per area, so browsing never waits for the order path.
- Place and pay. Placing the order creates it in the order service, which takes the payment at once, at checkout.
- To the kitchen. The order goes to the restaurant's app through the connection gateways, and a timer starts. The restaurant has 3 minutes to accept.
- Accepted. The restaurant accepts in time, and the order moves on. Had it stayed silent, the timer would have cancelled the order and refunded it.
- Dispatch. Dispatch learns when the food should be ready and chooses a rider, possibly one who also carries a second order.
- An offer. The offer goes to the rider's app through the same gateways, and the rider accepts the job.
- Pickup and delivery. The rider's app confirms the pickup and the delivery, while the customer follows the rider's position.
- The same events. Every change writes an outbox row, so notifications and delivery estimates follow from the same events as the order itself.
Checkpoint
Check the first design
The order’s states are the contract between the three parties
The program plays two orders of 380, paid at checkout, through one table of moves. Every command carries an idempotency key, and the 3-minute timer is a command too:
Python 3.14 · main.py
PAID = 380
# (state, command): the next state; other moves are refused
NEXT = {
("placed", "accept"): "accepted",
("accepted", "cook"): "cooking",
("cooking", "ready"): "ready",
("ready", "collect"): "collected",
("collected", "deliver"): "delivered",
("placed", "cancel"): "cancelled",
("accepted", "cancel"): "cancelled",
("placed", "timer"): "cancelled", # not accepted within 3 minutes
}
# each step: minute, idempotency key, command
ORDERS = {
"1. Kitchen to door": "1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, "
"16 r1 ready, 19 p1 collect, 34 d1 deliver",
"2. Accepted too late": "3 t1 timer, 4 a1 accept",
}
def run(steps: str) -> None:
state, results = "placed", {}
for step in steps.split(", "):
minute, key, command = step.split()
target = NEXT.get((state, command))
if key in results:
result = results[key] + " (repeat)"
elif target is None:
result = f"refused: the order is {state}" # a refusal is never kept
else:
state = target
refund = f", refund {PAID}" if target == "cancelled" else ""
result = results[key] = f"ok: {target}{refund}"
print(f" {minute:>3} {key} {command:<8} -> {result}")
for number, (title, steps) in enumerate(ORDERS.items()):
print(f"\n{title}" if number else title)
run(steps) In Python: The table is a dict keyed by (state, command) tuples, so NEXT.get((state, command)) is the whole rule, and result = results[key] = ... keeps and names the first answer at once.
Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py
JavaScript Node 24 · main.mjs
const PAID = 380;
// (state, command): the next state; other moves are refused
const NEXT = new Map([
["placed accept", "accepted"],
["accepted cook", "cooking"],
["cooking ready", "ready"],
["ready collect", "collected"],
["collected deliver", "delivered"],
["placed cancel", "cancelled"],
["accepted cancel", "cancelled"],
["placed timer", "cancelled"], // not accepted within 3 minutes
]);
// each step: minute, idempotency key, command
const ORDERS = [
[
"1. Kitchen to door",
"1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, " +
"16 r1 ready, 19 p1 collect, 34 d1 deliver",
],
["2. Accepted too late", "3 t1 timer, 4 a1 accept"],
];
function run(steps) {
let state = "placed";
const results = new Map();
for (const step of steps.split(", ")) {
const [minute, key, command] = step.split(" ");
const target = NEXT.get(`${state} ${command}`);
let result;
if (results.has(key)) result = `${results.get(key)} (repeat)`;
else if (target === undefined)
result = `refused: the order is ${state}`; // a refusal is never kept
else {
state = target;
result = `ok: ${target}${target === "cancelled" ? `, refund ${PAID}` : ""}`;
results.set(key, result);
}
console.log(` ${minute.padStart(3)} ${key} ${command.padEnd(8)} -> ${result}`);
}
}
ORDERS.forEach(([title, steps], number) => {
console.log(number ? `\n${title}` : title);
run(steps);
}); In JavaScript: A Map compares array keys by identity, so the table joins state and command into one string key, "placed accept", and get returns undefined for a move that is not allowed.
Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs
Java 25 LTS · Main.java
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class Main {
static final int PAID = 380;
// (state, command): the next state; other moves are refused
static final Map<String, String> NEXT =
Map.of(
"placed accept", "accepted",
"accepted cook", "cooking",
"cooking ready", "ready",
"ready collect", "collected",
"collected deliver", "delivered",
"placed cancel", "cancelled",
"accepted cancel", "cancelled",
"placed timer", "cancelled"); // not accepted within 3 minutes
static void run(String steps) {
String state = "placed";
Map<String, String> results = new HashMap<>();
for (String step : steps.split(", ")) {
String[] part = step.split(" ");
String key = part[1];
String target = NEXT.get(state + " " + part[2]);
String result;
if (results.containsKey(key)) {
result = results.get(key) + " (repeat)";
} else if (target == null) {
result = "refused: the order is " + state; // a refusal is never kept
} else {
state = target;
result = "ok: " + target + (target.equals("cancelled") ? ", refund " + PAID : "");
results.put(key, result);
}
System.out.printf(" %3s %s %-8s -> %s%n", part[0], key, part[2], result);
}
}
public static void main(String[] args) {
Locale.setDefault(Locale.ROOT);
// each step: minute, idempotency key, command
String[][] orders = {
{
"1. Kitchen to door",
"1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, 16 r1 ready, 19 p1 collect, "
+ "34 d1 deliver"
},
{"2. Accepted too late", "3 t1 timer, 4 a1 accept"}
};
for (int i = 0; i < orders.length; i++) {
System.out.println((i > 0 ? "\n" : "") + orders[i][0]);
run(orders[i][1]);
}
}
} In Java: Map.of takes up to ten pairs, enough for the eight moves, and builds a table no code can change while orders run.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java
C++23 · main.cpp
#include <map>
#include <print>
#include <sstream>
#include <string>
#include <utility>
constexpr int PAID = 380;
// (state, command): the next state; other moves are refused
const std::map<std::pair<std::string, std::string>, std::string> NEXT{
{{"placed", "accept"}, "accepted"},
{{"accepted", "cook"}, "cooking"},
{{"cooking", "ready"}, "ready"},
{{"ready", "collect"}, "collected"},
{{"collected", "deliver"}, "delivered"},
{{"placed", "cancel"}, "cancelled"},
{{"accepted", "cancel"}, "cancelled"},
{{"placed", "timer"}, "cancelled"}, // not accepted within 3 minutes
};
void run(const std::string &steps) {
std::string state = "placed", key, command, result;
int minute = 0;
std::map<std::string, std::string> results;
std::istringstream in(steps);
while (in >> minute >> key >> command) {
if (command.ends_with(',')) {
command.pop_back(); // the comma after the step
}
const auto target = NEXT.find({state, command});
if (results.contains(key)) {
result = results[key] + " (repeat)";
} else if (target == NEXT.end()) {
result = "refused: the order is " + state; // a refusal is never kept
} else {
state = target->second;
result = "ok: " + state;
if (state == "cancelled") {
result += std::format(", refund {}", PAID);
}
results[key] = result;
}
std::println(" {:>3} {} {:<8} -> {}", minute, key, command, result);
}
}
int main() {
// each step: minute, idempotency key, command
const std::pair<std::string, std::string> orders[]{
{"1. Kitchen to door", "1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, "
"5 x1 cancel, 16 r1 ready, 19 p1 collect, 34 d1 "
"deliver"},
{"2. Accepted too late", "3 t1 timer, 4 a1 accept"},
};
for (std::size_t i = 0; i < std::size(orders); ++i) {
std::println("{}{}", i > 0 ? "\n" : "", orders[i].first);
run(orders[i].second);
}
} In C++: operator>> reads each step as three words, so the command keeps the comma that follows it, and ends_with(",") and pop_back remove it.
Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main
C# 14 · .NET 10 · app.cs
const int Paid = 380;
// (state, command): the next state; other moves are refused
Dictionary<(string, string), string> next = new()
{
[("placed", "accept")] = "accepted",
[("accepted", "cook")] = "cooking",
[("cooking", "ready")] = "ready",
[("ready", "collect")] = "collected",
[("collected", "deliver")] = "delivered",
[("placed", "cancel")] = "cancelled",
[("accepted", "cancel")] = "cancelled",
[("placed", "timer")] = "cancelled", // not accepted within 3 minutes
};
// each step: minute, idempotency key, command
(string Title, string Steps)[] orders =
[
("1. Kitchen to door", "1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, "
+ "16 r1 ready, 19 p1 collect, 34 d1 deliver"),
("2. Accepted too late", "3 t1 timer, 4 a1 accept"),
];
for (int i = 0; i < orders.Length; i++)
{
Console.WriteLine((i > 0 ? "\n" : "") + orders[i].Title);
Run(orders[i].Steps);
}
void Run(string steps)
{
string state = "placed";
Dictionary<string, string> results = [];
foreach (string step in steps.Split(", "))
{
string[] part = step.Split(' ');
(string minute, string key, string command) = (part[0], part[1], part[2]);
string result;
if (results.TryGetValue(key, out string? first)) result = $"{first} (repeat)";
else if (!next.TryGetValue((state, command), out string? target))
result = $"refused: the order is {state}"; // a refusal is never kept
else
{
state = target;
result = $"ok: {target}" + (target == "cancelled" ? $", refund {Paid}" : "");
results[key] = result;
}
Console.WriteLine($" {minute,3} {key} {command,-8} -> {result}");
}
} In C#: TryGetValue on a dictionary keyed by a (string, string) tuple tests and reads a move at once, so the branch that applies it already holds the target.
Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs
C23 · main.c
#include <stdio.h>
#include <string.h>
enum { PAID = 380, KEYS = 8, TEXT = 64 };
// (state, command): the next state; other moves are refused
static const char *const NEXT[][3] = {
{"placed", "accept", "accepted"},
{"accepted", "cook", "cooking"},
{"cooking", "ready", "ready"},
{"ready", "collect", "collected"},
{"collected", "deliver", "delivered"},
{"placed", "cancel", "cancelled"},
{"accepted", "cancel", "cancelled"},
{"placed", "timer", "cancelled"}, // not accepted within 3 minutes
};
static void run(const char *steps) {
const char *state = "placed";
char keys[KEYS][4], results[KEYS][TEXT];
int kept = 0;
for (const char *step = steps; *step != '\0';) {
char key[4] = "", command[12] = "", result[TEXT] = "";
int minute = 0, used = 0, first = -1;
sscanf(step, "%d %3s %11[^,]%n", &minute, key, command, &used);
step += used + strspn(step + used, ", ");
const char *target = NULL;
for (size_t i = 0; i < sizeof NEXT / sizeof NEXT[0]; i++) {
if (strcmp(NEXT[i][0], state) == 0 && strcmp(NEXT[i][1], command) == 0) {
target = NEXT[i][2];
}
}
for (int i = 0; i < kept; i++) {
if (strcmp(keys[i], key) == 0) {
first = i;
}
}
if (first >= 0) {
snprintf(result, TEXT, "%s (repeat)", results[first]);
} else if (target == NULL) {
// a refusal is never kept
snprintf(result, TEXT, "refused: the order is %s", state);
} else {
state = target;
const int end = snprintf(result, TEXT, "ok: %s", target);
if (strcmp(target, "cancelled") == 0) {
snprintf(result + end, TEXT - end, ", refund %d", PAID);
}
snprintf(keys[kept], sizeof keys[0], "%s", key);
snprintf(results[kept++], TEXT, "%s", result);
}
printf(" %3d %s %-8s -> %s\n", minute, key, command, result);
}
}
int main(void) {
// each step: minute, idempotency key, command
static const char *const ORDERS[][2] = {
{"1. Kitchen to door", "1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, "
"5 x1 cancel, 16 r1 ready, 19 p1 collect, 34 d1 "
"deliver"},
{"2. Accepted too late", "3 t1 timer, 4 a1 accept"},
};
for (size_t i = 0; i < sizeof ORDERS / sizeof ORDERS[0]; i++) {
printf("%s%s\n", i > 0 ? "\n" : "", ORDERS[i][0]);
run(ORDERS[i][1]);
}
return 0;
} In C: sscanf reads the command with %11[^,], which stops at the comma between steps, and snprintf returns the length it wrote, so the refund is appended after it.
Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main
PHP 8.5 · main.php
<?php
declare(strict_types=1);
const PAID = 380;
// (state, command): the next state; other moves are refused
const NEXT = [
'placed accept' => 'accepted',
'accepted cook' => 'cooking',
'cooking ready' => 'ready',
'ready collect' => 'collected',
'collected deliver' => 'delivered',
'placed cancel' => 'cancelled',
'accepted cancel' => 'cancelled',
'placed timer' => 'cancelled', // not accepted within 3 minutes
];
// each step: minute, idempotency key, command
const ORDERS = [
'1. Kitchen to door' => '1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, '
. '16 r1 ready, 19 p1 collect, 34 d1 deliver',
'2. Accepted too late' => '3 t1 timer, 4 a1 accept',
];
function run(string $steps): void
{
$state = 'placed';
$results = [];
foreach (explode(', ', $steps) as $step) {
[$minute, $key, $command] = explode(' ', $step);
$target = NEXT["$state $command"] ?? null;
if (isset($results[$key])) {
$result = "$results[$key] (repeat)";
} elseif ($target === null) {
$result = "refused: the order is $state"; // a refusal is never kept
} else {
$state = $target;
$refund = $target === 'cancelled' ? ', refund ' . PAID : '';
$result = $results[$key] = "ok: $target$refund";
}
printf(" %3s %s %-8s -> %s\n", $minute, $key, $command, $result);
}
}
$number = 0;
foreach (ORDERS as $title => $steps) {
echo ($number++ > 0 ? "\n" : '') . "$title\n";
run($steps);
} In PHP: NEXT["$state $command"] ?? null reads a move or null, and "$results[$key] (repeat)" interpolates an array element straight into the string.
Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php
Rust 1.99 · main.rs
use std::collections::HashMap;
const PAID: u32 = 380;
// (state, command): the next state; other moves are refused
const NEXT: [(&str, &str, &str); 8] = [
("placed", "accept", "accepted"),
("accepted", "cook", "cooking"),
("cooking", "ready", "ready"),
("ready", "collect", "collected"),
("collected", "deliver", "delivered"),
("placed", "cancel", "cancelled"),
("accepted", "cancel", "cancelled"),
("placed", "timer", "cancelled"), // not accepted within 3 minutes
];
fn run(steps: &str) {
let mut state = "placed";
let mut results: HashMap<&str, String> = HashMap::new();
for step in steps.split(", ") {
let part: Vec<&str> = step.split(' ').collect();
let (minute, key, command) = (part[0], part[1], part[2]);
let target = NEXT.iter().find(|m| (m.0, m.1) == (state, command));
let result = if let Some(first) = results.get(key) {
format!("{first} (repeat)")
} else if let Some(&(_, _, next)) = target {
state = next;
let refund = if next == "cancelled" {
format!(", refund {PAID}")
} else {
String::new()
};
let result = format!("ok: {next}{refund}");
results.insert(key, result.clone());
result
} else {
format!("refused: the order is {state}") // a refusal is never kept
};
println!(" {minute:>3} {key} {command:<8} -> {result}");
}
}
fn main() {
// each step: minute, idempotency key, command
let orders = [
(
"1. Kitchen to door",
"1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, 16 r1 ready, \
19 p1 collect, 34 d1 deliver",
),
("2. Accepted too late", "3 t1 timer, 4 a1 accept"),
];
for (number, (title, steps)) in orders.iter().enumerate() {
println!("{}{title}", if number > 0 { "\n" } else { "" });
run(steps);
}
} In Rust: The table is an array of tuples searched with find, and a trailing backslash continues the long string literal on the next line without a newline.
Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main
TypeScript 6.0 · main.ts
const PAID = 380;
// (state, command): the next state; other moves are refused
const NEXT = new Map<string, string>([
["placed accept", "accepted"],
["accepted cook", "cooking"],
["cooking ready", "ready"],
["ready collect", "collected"],
["collected deliver", "delivered"],
["placed cancel", "cancelled"],
["accepted cancel", "cancelled"],
["placed timer", "cancelled"], // not accepted within 3 minutes
]);
// each step: minute, idempotency key, command
const ORDERS: [string, string][] = [
[
"1. Kitchen to door",
"1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, " +
"16 r1 ready, 19 p1 collect, 34 d1 deliver",
],
["2. Accepted too late", "3 t1 timer, 4 a1 accept"],
];
function run(steps: string): void {
let state = "placed";
const results = new Map<string, string>();
for (const step of steps.split(", ")) {
const [minute, key, command] = step.split(" ");
const target = NEXT.get(`${state} ${command}`);
const first = results.get(key);
let result: string;
if (first !== undefined) result = `${first} (repeat)`;
else if (target === undefined)
result = `refused: the order is ${state}`; // a refusal is never kept
else {
state = target;
result = `ok: ${target}${target === "cancelled" ? `, refund ${PAID}` : ""}`;
results.set(key, result);
}
console.log(` ${minute.padStart(3)} ${key} ${command.padEnd(8)} -> ${result}`);
}
}
ORDERS.forEach(([title, steps], number) => {
console.log(number ? `\n${title}` : title);
run(steps);
}); In TypeScript: Map.get returns string | undefined, so a kept answer and a target are each tested against undefined before use, which narrows them to string.
Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts
Go 1.27 · main.go
package main
import (
"fmt"
"strings"
)
const paid = 380
// (state, command): the next state; other moves are refused
var next = map[[2]string]string{
{"placed", "accept"}: "accepted",
{"accepted", "cook"}: "cooking",
{"cooking", "ready"}: "ready",
{"ready", "collect"}: "collected",
{"collected", "deliver"}: "delivered",
{"placed", "cancel"}: "cancelled",
{"accepted", "cancel"}: "cancelled",
{"placed", "timer"}: "cancelled", // not accepted within 3 minutes
}
func run(steps string) {
state := "placed"
results := map[string]string{}
for _, step := range strings.Split(steps, ", ") {
part := strings.Fields(step)
minute, key, command := part[0], part[1], part[2]
target, legal := next[[2]string{state, command}]
result, repeat := results[key]
switch {
case repeat:
result += " (repeat)"
case !legal:
result = "refused: the order is " + state // a refusal is never kept
default:
state = target
result = "ok: " + target
if target == "cancelled" {
result += fmt.Sprintf(", refund %d", paid)
}
results[key] = result
}
fmt.Printf(" %3s %s %-8s -> %s\n", minute, key, command, result)
}
}
func main() {
// each step: minute, idempotency key, command
orders := [][2]string{
{"1. Kitchen to door", "1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, " +
"16 r1 ready, 19 p1 collect, 34 d1 deliver"},
{"2. Accepted too late", "3 t1 timer, 4 a1 accept"},
}
for i, order := range orders {
if i > 0 {
fmt.Println()
}
fmt.Println(order[0])
run(order[1])
}
} In Go: A map lookup's second value says whether the key was there, so results[key] tells a repeated key apart and next[...] a legal move from a missing one.
Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go
Swift 6.4 · main.swift
let paid = 380
// (state, command): the next state; other moves are refused
let next = [
"placed accept": "accepted",
"accepted cook": "cooking",
"cooking ready": "ready",
"ready collect": "collected",
"collected deliver": "delivered",
"placed cancel": "cancelled",
"accepted cancel": "cancelled",
"placed timer": "cancelled", // not accepted within 3 minutes
]
// each step: minute, idempotency key, command
let orders = [
(
"1. Kitchen to door",
"1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, 16 r1 ready, 19 p1 collect, "
+ "34 d1 deliver"
),
("2. Accepted too late", "3 t1 timer, 4 a1 accept"),
]
func run(_ steps: String) {
var state = "placed"
var results: [Substring: String] = [:]
for step in steps.split(separator: ",") {
let part = step.split(separator: " ")
let (minute, key, command) = (String(part[0]), part[1], String(part[2]))
let result: String
if let first = results[key] {
result = first + " (repeat)"
} else if let target = next["\(state) \(command)"] {
state = target
result = "ok: \(target)" + (target == "cancelled" ? ", refund \(paid)" : "")
results[key] = result
} else {
result = "refused: the order is \(state)" // a refusal is never kept
}
let at = String(repeating: " ", count: 3 - minute.count) + minute
let padded = command + String(repeating: " ", count: 8 - command.count)
print(" \(at) \(key) \(padded) -> \(result)")
}
}
for (number, (title, steps)) in orders.enumerated() {
print(number > 0 ? "\n\(title)" : title)
run(steps)
} In Swift: Dictionary keys must be Hashable and tuples are not, so the table is keyed by state and command joined into one string.
Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main
Ruby 4.0 · main.rb
# frozen_string_literal: true
PAID = 380
# (state, command): the next state; other moves are refused
NEXT = {
%w[placed accept] => 'accepted',
%w[accepted cook] => 'cooking',
%w[cooking ready] => 'ready',
%w[ready collect] => 'collected',
%w[collected deliver] => 'delivered',
%w[placed cancel] => 'cancelled',
%w[accepted cancel] => 'cancelled',
%w[placed timer] => 'cancelled' # not accepted within 3 minutes
}.freeze
# each step: minute, idempotency key, command
ORDERS = {
'1. Kitchen to door' => '1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, ' \
'16 r1 ready, 19 p1 collect, 34 d1 deliver',
'2. Accepted too late' => '3 t1 timer, 4 a1 accept'
}.freeze
# an order's state and each key's first result
class Order
def initialize
@state = 'placed'
@results = {}
end
def handle(key, command)
return "#{@results[key]} (repeat)" if @results.key?(key)
target = NEXT[[@state, command]]
return "refused: the order is #{@state}" unless target # a refusal is never kept
@state = target
refund = target == 'cancelled' ? ", refund #{PAID}" : ''
@results[key] = "ok: #{target}#{refund}"
end
end
ORDERS.each_with_index do |(title, steps), number|
puts number.positive? ? "\n#{title}" : title
order = Order.new
steps.split(', ').each do |step|
minute, key, command = step.split
puts " #{minute.rjust(3)} #{key} #{command.ljust(8)} -> #{order.handle(key, command)}"
end
end In Ruby: The table is keyed by two-word arrays such as %w[placed accept], and an Order object holds the state, so no method passes RuboCop's ten-line limit.
Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb
Kotlin 2.4 · Main.kt
const val PAID = 380
// (state, command): the next state; other moves are refused
val next =
mapOf(
("placed" to "accept") to "accepted",
("accepted" to "cook") to "cooking",
("cooking" to "ready") to "ready",
("ready" to "collect") to "collected",
("collected" to "deliver") to "delivered",
("placed" to "cancel") to "cancelled",
("accepted" to "cancel") to "cancelled",
("placed" to "timer") to "cancelled", // not accepted within 3 minutes
)
// each step: minute, idempotency key, command
val orders =
listOf(
"1. Kitchen to door" to
"1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, 16 r1 ready, " +
"19 p1 collect, 34 d1 deliver",
"2. Accepted too late" to "3 t1 timer, 4 a1 accept",
)
fun run(steps: String) {
var state = "placed"
val results = mutableMapOf<String, String>()
for (step in steps.split(", ")) {
val (minute, key, command) = step.split(" ")
val target = next[state to command]
val result =
when {
key in results -> {
"${results.getValue(key)} (repeat)"
}
target == null -> {
"refused: the order is $state"
}
// a refusal is never kept
else -> {
state = target
val refund = if (target == "cancelled") ", refund $PAID" else ""
"ok: $target$refund".also { results[key] = it }
}
}
println(" ${minute.padStart(3)} $key ${command.padEnd(8)} -> $result")
}
}
fun main() {
for ((number, order) in orders.withIndex()) {
println(if (number > 0) "\n${order.first}" else order.first)
run(order.second)
}
} In Kotlin: The table is keyed by a Pair, state to command, and when returns the result of the first case that holds, which also keeps the answer under its key.
Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
Dart 3.13 · main.dart
const paid = 380;
// (state, command): the next state; other moves are refused
final next = {
('placed', 'accept'): 'accepted',
('accepted', 'cook'): 'cooking',
('cooking', 'ready'): 'ready',
('ready', 'collect'): 'collected',
('collected', 'deliver'): 'delivered',
('placed', 'cancel'): 'cancelled',
('accepted', 'cancel'): 'cancelled',
('placed', 'timer'): 'cancelled', // not accepted within 3 minutes
};
// each step: minute, idempotency key, command
const orders = [
(
'1. Kitchen to door',
'1 a1 accept, 1 a1 accept, 2 c1 cook, 3 t1 timer, 5 x1 cancel, '
'16 r1 ready, 19 p1 collect, 34 d1 deliver',
),
('2. Accepted too late', '3 t1 timer, 4 a1 accept'),
];
void run(String steps) {
var state = 'placed';
final results = <String, String>{};
for (final step in steps.split(', ')) {
final [minute, key, command] = step.split(' ');
final target = next[(state, command)];
final String result;
if (results[key] case final first?) {
result = '$first (repeat)';
} else if (target == null) {
result = 'refused: the order is $state'; // a refusal is never kept
} else {
state = target;
final refund = target == 'cancelled' ? ', refund $paid' : '';
result = results[key] = 'ok: $target$refund';
}
print(' ${minute.padLeft(3)} $key ${command.padRight(8)} -> $result');
}
}
void main() {
for (final (number, (title, steps)) in orders.indexed) {
print(number > 0 ? '\n$title' : title);
run(steps);
}
} In Dart: Records compare by value, so (state, command) is a key as it is, and if (results[key] case final first?) reads a kept answer only when there is one.
Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart
What every language printed
1. Kitchen to door
1 a1 accept -> ok: accepted
1 a1 accept -> ok: accepted (repeat)
2 c1 cook -> ok: cooking
3 t1 timer -> refused: the order is cooking
5 x1 cancel -> refused: the order is cooking
16 r1 ready -> ok: ready
19 p1 collect -> ok: collected
34 d1 deliver -> ok: delivered
2. Accepted too late
3 t1 timer -> ok: cancelled, refund 380
4 a1 accept -> refused: the order is cancelled
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 103 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 158 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 3 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 18 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 32 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 724 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 196 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 18 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
The tablet’s accept was sent twice and applied once: the second copy carried the same key and got the first answer back. The timer found the first order cooking at minute 3 and changed nothing, so a late timer can never cancel an accepted order, and the customer’s cancel at minute 5 was refused, because a customer can cancel only until the kitchen starts. In the second order the restaurant never answered: the timer cancelled it with a full refund of 380, and the accept that came a minute later was refused.
Cancel before the kitchen starts
Press Edit in the Python tab and change 2 c1 cook to 2 x1 cancel in the first order: the cancel now applies with a refund of 380, its copy at minute 5 gets that first answer back, and the restaurant’s and the rider’s later commands are refused.
An item the kitchen runs out of is removed and refunded on its own, while the rest of the order goes on. These are the compensations of a saga: A business transaction across several services, run as a sequence of local transactions, each paired with a compensating transaction that undoes its effect if a later step fails. More in “Sagas: orchestration, choreography, compensation”: the payment is taken at the start, so every later failure undoes exactly what it must, and no more (Microsoft). Stripe’s API reference describes what the key buys at checkout: the result of the first request with a key is saved and returned to every retry, so a customer who taps twice places one order (Stripe).
Checkpoint
Check: The order's states, keys, the timer and refunds
Practise: the order, from scope to state machine
Rehearse the estimates with the exercise above, then take questions on the scope, the first design and the order’s states, your weakest part first:
Practice that adapts to you
Practise this lesson
11 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
The deep dives this design must answer
- Dispatch timing: choosing the rider when the order is placed, or just before the food is ready.
- Batching: giving one rider two orders that are ready together and go the same way.
- ETAs: adding preparation time, riding time and handovers into a promise that holds.
- Delivery areas: each restaurant’s area shrinks when riders are short or roads are slow.
- Peaks: lunch, dinner and rain need riders by Little’s law, early scaling and shedding.
- Failure drills: what each component’s failure does to orders in progress, and what limits it.
An answer is judged on the scope and estimates, the order’s states, dispatch and batching, ETAs, peaks and delivery areas, and failures.
Assigning and batching deliveries, measured Pro: opens with a Pro, Premium or Ultimate pass
Who delivers an order, and when that rider is chosen, decides both the customer’s wait and how much of every rider’s hour is wasted. This part measures three dispatch policies on one simulated dinner hour and turns the results into rules.
In this part:
- Three dispatch policies on one dinner hour, measured
- Batching without hurting the first customer
- When the problem becomes vehicle routing
- ETAs that customers can trust
1 diagram · 1 runnable example (Python) · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Read a complete sample first: Design ride hailing like Uber or Ola: matching
Lunch, dinner and rain: riders and delivery areas, with numbers Pro: opens with a Pro, Premium or Ultimate pass
Peaks are when a food platform does most of its business and when it most often lets customers down, because orders rise just as riders and roads slow down. This part sizes riders for a rainy dinner with Little’s law, shows what shrinking delivery areas buys, and lists what to shed first.
In this part:
- Riders needed in the rain, by Little’s law
- Delivery areas that shrink and grow
- Scaling early and shedding the right things
1 runnable example (Python) · about 7 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Failure drills, scale, the level rubric and a self-mock Pro: opens with a Pro, Premium or Ultimate pass
Food delivery meets its failures at dinner time, when every order in flight is someone waiting. This part drills those failures, scales the design to 10× and 100×, and sets out what each level of candidate should cover, with a sample answer and a practice plan.
In this part:
- Failure drills
- Bottlenecks at 10× and 100×
- Alternatives considered and rejected
- What each level is expected to cover
- A sample answer and a 45-minute self-mock
- What this part adds up to
about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
The first question below is answered in full. For the rainy dinner and the two-order batch, choose your opening move in the briefs, then read the full questions, which build on it:
Checkpoint
Your first decision
-
Fresher to mid · Warm-up
Why does a food delivery order need a timeout for the restaurant to accept it?
The customer has already paid and is waiting, and the restaurant may be closed, overloaded or simply not looking at its tablet. Without a deadline the order could sit unanswered while the food the customer wanted is never cooked. A timer of a few minutes turns silence into a decision: the order is cancelled and refunded in full, the customer can order elsewhere, and the restaurant's late accept is refused because the order has already moved on. The same timer is also a signal: restaurants that often let orders time out can be paused for new orders.
-
Senior to staff
It starts raining at dinner time in a big city. Orders rise by 40 %, 30 % of riders log off and traffic slows. What does the platform do in the next 30 minutes, and how does it decide how far to go?
-
Mid to senior
When should one rider carry two orders? Describe the rule you would use in real time, and put numbers on what it costs the first customer and what it saves the platform.
-
Mid to senior
A busy restaurant's tablet goes offline during dinner with 12 orders in progress: some placed, some accepted, some being cooked. What happens to each order, and what do you change in the design?
Key takeaways
- One order state machine is shared by the customer, the restaurant and the rider; every command is idempotent.
- Payment is taken at checkout, so cancellations, timeouts and missing items are refunds, the compensations of a saga.
- The restaurant must accept within minutes, or the order is cancelled and refunded automatically.
- Little’s law links orders a minute, minutes per order and riders needed; it is the first estimate of the design.
References
- OR-Tools: Vehicle routing (Google for Developers)
- OR-Tools: Vehicle routing problem (Google for Developers)
- OR-Tools: Pickups and deliveries (Google for Developers)
- H3 documentation: Regions (polygonToCells) (H3 project)
- Saga design pattern (Azure Architecture Center) (Microsoft)
- Idempotent requests (API reference) (Stripe)
- Text analysis (Elastic)
- Site Reliability Engineering, chapter 21: Handling Overload (Google (O'Reilly Media))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress