Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free core, Pro depth: 9 of 25 minutes free
Free: Three parties move one order, and each sees the same state · About 167 orders a second need some 294,000 riders at dinner · One order moves through browsing, the kitchen and a rider · 3 more sections. With a pass: 3 in-depth parts and 3 interview answers, about 16 minutes.

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

  1. One question When does this design take the customer's payment, and what follows from that?

    Choose one answer.

    Show the answer: One question

    Answer: At checkout, when the order is placed, so every later failure is undone by a refund

    Taking payment at checkout makes refunds the compensations of the flow: the order can fail at several points, and each failure has its own refund.

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 carries busiest_hour_share of 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 for minutes_per_order minutes and riders are planned to be busy at most busy_share of the time.
  • order_storage_tb_per_year(orders_per_day, bytes_per_order, copies): the storage a year of orders needs, kept in copies copies.

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.

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

  1. Question 1 of 2 4 million orders a day, 15 % of them in the busiest dinner hour. How many orders a second is that hour?

    Type a number.

    A hint

    An hour has 3,600 seconds.

    Show the answer: Question 1 of 2

    Answer: 167 orders a second (anything from 150 to 184 counts)

    4,000,000 × 0.15 = 600,000 orders in the hour, and ÷ 3,600 ≈ 167 a second, 10,000 a minute.

  2. Question 2 of 2 10,000 orders a minute each keep a rider busy for 25 minutes, and riders can be busy 85 % of the time. How many riders must be online?

    Type a number.

    A hint

    Little's law: busy riders = orders a minute × minutes per order.

    Show the answer: Question 2 of 2

    Answer: 294000 riders (anything from 264600 to 323400 counts)

    10,000 × 25 = 250,000 riders busy at once, and ÷ 0.85 ≈ 294,000 online: about the 300,000 the platform has, so anything that lengthens a delivery needs riders that do not exist.

One order moves through browsing, the kitchen and a rider

Food delivery: customer app, API gateway, cached menus and areas, order service, dispatch, rider locations, restaurant and rider apps.Customer appAPI gatewayRestaurants, menus anddelivery areascached per areaOrder servicestate machine,payments, outboxDispatchassign and batchRider locationsin memoryConnection gatewaysRestaurant appRider appbrowseplace orderready timeupdatesoffersaccept, readypick up, deliver

The first complete design of food delivery

Text description of the diagram

The diagram shows the parts of the design from top to bottom.

  1. The customer app calls the API gateway. Browsing reads restaurants, menus and delivery areas, cached per area.
  2. 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.
  3. 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.
  4. 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:

Interactive · step by step One order through the first design Step through one order, from browsing to the door, and see which part does each step.
One order through the first design, step 8 of 8. Showing Customer app, Menu caches, Order service (accepted), Payment (paid at checkout), Connection gateways, Restaurant app, Dispatch, Rider app (picked up, then delivered). In focus: Order service.browseplaceCustomer appMenu cachesper areaOrder serviceacceptedPaymentpaid at checkoutConnection gatewaysRestaurant appDispatchRider apppicked up, then delivered

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Browse. The customer's app reads restaurants, menus and delivery areas from caches kept per area, so browsing never waits for the order path.
  2. Place and pay. Placing the order creates it in the order service, which takes the payment at once, at checkout.
  3. 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.
  4. 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.
  5. Dispatch. Dispatch learns when the food should be ready and chooses a rider, possibly one who also carries a second order.
  6. An offer. The offer goes to the rider's app through the same gateways, and the rider accepts the job.
  7. Pickup and delivery. The rider's app confirms the pickup and the delivery, while the customer follows the rider's position.
  8. 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

  1. One question A restaurant receives an order and never answers. What does the first design do, and when?

    Choose one answer.

    Show the answer: One question

    Answer: The timer cancels the order 3 minutes after it reached the restaurant and refunds it in full

    The restaurant must accept within 3 minutes, or the order is cancelled and refunded; its accept after that is refused, because the order has moved on.

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:

One program · 14 languages The order's state machine: keyed commands and a timer one table lookup per command

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

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

  1. Question 1 of 2 In the first order, the timer arrives at minute 3, when the order is already cooking. What does it do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing: the table has no timer move from cooking, so it is refused

    Treating the timer as a command that must be legal in the current state removes a classic bug: a late timer cancelling an order that was accepted a moment before it fired.

  2. Question 2 of 2 The customer's cancel arrives at minute 5, while the first order is cooking. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It is refused, because a customer can cancel only until the kitchen starts cooking

    The order's states and the rules for leaving them are the contract between the three parties, and the table states that contract in one place.

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.

  1. Question 1 of 11 A customer taps Place order twice on a slow network. What keeps it to one order and one payment?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: Both requests carry the same Idempotency-Key, so the second gets the first request's saved result

    POST /v1/orders takes the items, the address and an Idempotency-Key, so a retry places the same order.

  2. Question 2 of 11 Why must the customer, the restaurant and the rider see the same state of an order?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: Each acts on it, so a cancelled order must stop the kitchen and the rider as well as the customer's screen

    One order state machine shared by all three parties is the heart of the design.

  3. Question 3 of 11 300,000 riders online each report their position every 5 seconds. How many reports a second is that?

    Type a number.

    Show the answer: Question 3 of 11

    Answer: 60000 reports a second (anything from 54000 to 66000 counts)

    300,000 ÷ 5 = 60,000 reports a second, kept in memory as in ride matching.

  4. Question 4 of 11 4 million orders a day, 10 KB each, kept for a year in 3 copies. How many terabytes is that?

    Type a number.

    Show the answer: Question 4 of 11

    Answer: 44 TB (anything from 39.6 to 48.4 counts)

    10,000 bytes × 4,000,000 × 365 × 3 ≈ 43.8 TB, about 44.

  5. Question 5 of 11 Rain makes each order keep a rider busy 30 minutes instead of 25, with the same 10,000 orders a minute and riders busy 85 % of the time. How many riders are needed now?

    Type a number.

    Show the answer: Question 5 of 11

    Answer: 352941 riders (anything from 317941 to 387941 counts)

    10,000 × 30 ÷ 0.85 ≈ 353,000, against about 300,000 online: five minutes more per order needs some 59,000 riders that do not exist, which is why rain is a capacity problem.

  6. Question 6 of 11 Why does browsing read restaurants, menus and delivery areas from caches per area?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: Browsing is read-heavy, about 3,300 reads a second at the peak, and must never wait for the order path

    About 20 menu and restaurant views per order make browsing the heaviest read path of the design.

  7. Question 7 of 11 How do notifications and delivery estimates learn that an order has changed?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: Every change writes an outbox row, so they follow from the same events as the order

    Notifications and ETAs are consumers of the order's events, not parts of the order service.

  8. Question 8 of 11 The restaurant's tablet sends accept twice with the same key. What does the second copy get?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: The first answer back, and the order moves once

    In the run, the second accept at minute 1 printed the first answer, marked as a repeat.

  9. Question 9 of 11 In the second order the restaurant never answers, and the timer fires at minute 3. What does the customer get back?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: The full 380, because the order is cancelled before anything was cooked

    The restaurant's accept a minute later is refused, because the order is already cancelled.

  10. Question 10 of 11 After an order was accepted, the kitchen runs out of one of its three items. What should happen?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: That item is removed and refunded on its own, while the rest of the order goes on

    Payment is taken at the start, so every later failure undoes exactly what it must.

  11. Question 11 of 11 Can a customer cancel an order that the restaurant has not accepted yet?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: Yes, with a full refund, as they can until the kitchen starts cooking

    In the try above, a cancel before cooking applied with a refund of 380, and the restaurant's and the rider's later commands were refused.

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.

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

  1. Question 1 of 2 It starts raining at dinner in a big city: orders rise by 40 %, 30 % of riders log off and traffic slows. What is your first move?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Work out, per zone, the riders the orders now need by Little's law, and compare it with the riders online

    Riders needed = orders a minute × minutes each order keeps a rider busy ÷ the share of time riders can be busy, computed per zone every few minutes.

  2. Question 2 of 2 When should one rider carry two orders? What is your first move in answering?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: State when a second order fits, near the first restaurant, ready about together and going the same way, then bound the first customer's cost

    The rule has two halves: when a second order fits, and what the first customer may lose. Then measure delivery time and rider time to see whether the rule helped.

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

  2. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

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

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.