system-design

Design a Food Delivery Service (Talabat / Uber Eats / DoorDash)

Difficulty: Tier 3 Asked at: Talabat, Careem (Now), DoorDash, Grab, Amazon; a MENA/SEA favourite Time budget: 45–60 min

Food delivery is ride-hailing plus a three-sided marketplace (customer, restaurant, courier) and a multi-stage fulfillment problem. The geospatial matching is familiar, so this study focuses on what’s new: coordinating three parties, the order-and-delivery state machine, and the timing problem — when to assign a courier so the food is ready right as they arrive.

Prerequisites: Design Ride-Hailing, Geospatial Indexing, Sagas / Distributed Transactions


1. Requirements

Functional:

Non-functional:

Out of scope: payment internals (separate), restaurant recommendations/search (reference feed/typeahead).


2. Estimation


3. The three-sided coordination

🚨 Three actors, each with their own app and state, must stay consistent:

flowchart LR
    Customer -->|1. order + pay| Order[Order Service]
    Order -->|2. send order| Restaurant[Restaurant app]
    Restaurant -->|3. accept, cooking, ready| Order
    Order -->|4. find courier| Match[Courier Matching<br/>geospatial]
    Match --> Courier[Courier app]
    Courier -->|5. pickup → dropoff| Order
    Order -->|live status/location| Customer

The order service is the orchestrator holding the source-of-truth state machine; the three apps are clients that update and observe it. Live updates flow over persistent connections (chat/ride pattern).


4. The order & delivery state machine

A long-lived workflow spanning minutes and three parties:

PLACED → (payment authorized) → RESTAURANT_ACCEPTED → PREPARING → READY
       → COURIER_ASSIGNED → PICKED_UP → EN_ROUTE → DELIVERED → (payment captured)

🚨 Each transition is a durable, ordered event. Failures at any stage need handling: restaurant rejects → refund + notify; no courier found → retry/escalate; customer cancels → compensations. This is a saga — a multi-step distributed transaction with compensating actions, not a single ACID transaction.


5. Deep dives

5a. Courier matching — and the timing problem

Finding a nearby courier is the ride-hailing geospatial problem (geohash/S2 index, nearby query, offer-and-accept). 🚨 The twist: when to assign. Assign too early and the courier waits at the restaurant for food; too late and the customer waits. Optimal dispatch predicts food-ready time and courier ETA so the courier arrives just as the food is ready. This prediction + optimization is what distinguishes a good delivery system. Also: batching — one courier carrying multiple nearby orders.

5b. Reliability via sagas

Because the order spans payment, restaurant, and courier — separate services — you can’t wrap it in one database transaction. Use a saga: each step commits locally and emits an event; if a later step fails, run compensating transactions (refund the payment, cancel the restaurant order, notify the customer). The order service (or a workflow engine) coordinates this. (Saga Pattern) This guarantees the system reaches a consistent end state (delivered or fully compensated) despite partial failures.

5c. Handling peak load

Meal times = massive predictable spikes. Autoscale services ahead of known peaks; use queues to absorb order bursts; degrade gracefully (extend ETAs, pause new orders in overloaded zones) rather than crash. Regional sharding (like ride-hailing) contains load per city.

5d. Real-time tracking

Customer watches: order status transitions + courier location on the map. Courier location streams via WebSocket (ride-hailing pattern); status transitions push to the customer app. Restaurant gets order alerts reliably (they must see the order — retry + audible alert + confirmation).

5e. Restaurant availability & menu

Restaurants go online/offline, run out of items, have prep-time variance. The catalog (menus, availability) is a read-heavy service (cache + CDN for menu images), updated by restaurants. Show accurate availability so customers don’t order unavailable items.


6. Bottlenecks & scaling further

  1. Peak spikes → autoscale + queues + graceful degradation; provision for peak.
  2. Courier matching → geospatial index + predictive dispatch + batching.
  3. Cross-service consistency → sagas with compensations.
  4. Live tracking → WebSocket connection servers.
  5. Menu reads → cache + CDN; independent read-heavy catalog service.

7. Trade-off summary

Decision Chosen Alternative Why
Order consistency Saga + compensations One ACID transaction Spans independent services; can’t be one txn
Courier assignment Predictive (food-ready + ETA) Assign immediately Minimizes both courier and customer waiting
Efficiency Batch nearby orders One order per trip More deliveries per courier
Partitioning By city/region Global Deliveries are local; contain peak load
Peak handling Autoscale + degrade Provision for average Meal-time spikes are huge and predictable

8. Follow-up questions

Why can't the whole order be one database transaction? Because it spans multiple independent services and external parties over several minutes: payment authorization, the restaurant accepting and cooking, courier assignment, pickup, and delivery. A single ACID transaction requires one database and short duration; here the steps live in different services (payments, restaurant, dispatch) and take minutes, with humans in the loop — you can't hold a database lock across a 20-minute cooking process. So you use a saga: each step commits locally and publishes an event, and if a later step fails (restaurant rejects, no courier available, customer cancels), the system runs compensating transactions to undo the completed steps (refund the payment, cancel the restaurant order, notify everyone). The saga guarantees the order reaches a consistent terminal state — either fully delivered or fully unwound — without needing a distributed transaction that would be impractical across these services and timescales.
What's the hardest part of courier assignment that ride-hailing doesn't have? Timing against food readiness. In ride-hailing the rider is ready the moment they request, so you just want the nearest available driver as fast as possible. In food delivery the food isn't ready when the order is placed — it needs preparation — so assigning the nearest courier immediately often means the courier arrives and waits idle at the restaurant, wasting their time, while assigning too late makes the finished food sit and the customer wait. The system has to predict when the food will actually be ready (which varies by restaurant, item, and current kitchen load) and the courier's travel time, and dispatch so the courier arrives just as the food is ready. Getting this prediction and optimization right — plus batching multiple nearby orders onto one courier — is what separates an efficient delivery platform from a mediocre one, and it's a dimension ride-hailing simply doesn't have.
Meal times cause huge spikes. How do you handle them? Since the spikes are large but predictable (lunch and dinner windows), you provision and autoscale for peak rather than average — scaling capacity up ahead of known meal times. Order bursts are absorbed by queues so a sudden flood doesn't overwhelm downstream services, and the system degrades gracefully under extreme load rather than failing: extending quoted ETAs, temporarily pausing new orders in overloaded zones, or throttling, so existing orders still complete. Regional sharding contains each city's peak to its own capacity so one hot market doesn't affect others. The philosophy is to plan for the known peak and shed or delay load gracefully at the extremes instead of crashing.
How do you make sure the restaurant actually sees an incoming order? Treat order delivery to the restaurant as a must-deliver, acknowledged action, not fire-and-forget. The order service sends the order to the restaurant's device/tablet and waits for an explicit acceptance acknowledgement; if it isn't acknowledged within a short window, it retries, escalates (louder/repeated alerts), and can fall back to alternative channels (a phone call, a support agent). The order stays in a "pending restaurant confirmation" state until acknowledged, and the customer's payment is only authorized (not captured) until the restaurant confirms — so an unseen order doesn't silently charge the customer or strand them. Reliable, acknowledged delivery with retries and escalation is essential because a missed order is a failed sale and a bad experience for all three parties.

9. What junior / mid / senior answers look like


Further reading