Design a Food Delivery Platform
A food delivery app connects three parties in real time: customers who want a meal, restaurants who prepare it, and couriers who carry it. It looks like ride sharing with a menu, and it is harder, because an order has a preparation phase with its own uncertainty, three actors must stay in agreement, and the quality of the experience depends on a good delivery time estimate. It is a rich interview problem because it combines browsing at scale, a transactional ordering flow, a state machine shared by several actors, dispatch, and live tracking.
The chapter follows the usual shape: understand the problem, set up the interface, build the high-level design, then go deep on the questions interviewers use to separate levels.
1. Understanding the problem
A customer finds a restaurant, builds a basket, pays, and watches the order progress until it arrives. The restaurant receives the order, accepts it and prepares it. A courier is assigned, collects it and delivers it.
Functional requirements
Core:
- Customers can browse and search restaurants and menus near them, with delivery time estimates and fees.
- Customers can place an order and pay.
- Restaurants receive, accept and update orders.
- Couriers are assigned, collect and deliver orders, and customers can track them live.
Confirm in or out: scheduled orders, group orders, promotions, ratings, multi-restaurant baskets, courier onboarding and payouts. A sensible opening: "I will design discovery, ordering, dispatch and live tracking for single-restaurant orders, and treat promotions, ratings and payouts as extensions."
Non-functional requirements
- Fast discovery. Menu and search pages are the most-used screens and must feel instant.
- Correct, durable orders. An order must never be lost or charged twice, and its state must be consistent for all three parties.
- Timeliness. A late or cold meal is the failure users remember, so dispatch and estimates matter.
- Availability at the busy lunch and dinner peaks, which are far above average.
- Scale. Assume 5 million orders a day.
Estimation
| Quantity | Calculation | Result |
|---|---|---|
| Orders | per day | about 58 per second average |
| Peak orders | most orders in two meal windows, say 40 percent in 2 hours | about 280 per second |
| Browsing | 30 menu or search views per order | about 1,700 per second average, 8,000 at peak |
| Active couriers | say 300,000 sending a location every 5 seconds | 60,000 location updates per second |
What the numbers say. Ordering volume is modest and easy for a transactional database. Browsing is a hundred times heavier and cacheable. Location updates are the highest write rate, and are ephemeral. The design therefore separates three load profiles: read-heavy discovery, low-volume transactional orders, and high-volume ephemeral location.
2. The set up
Core entities
- Restaurant, menu item (with price, availability, preparation time).
- Order and order item, with a state, totals and timestamps.
- Customer, courier and their locations.
- Delivery: the pickup and drop-off task for a courier.
API
GET /v1/restaurants?lat=..&lng=..&query=.. -> restaurants with ETA and fee
GET /v1/restaurants/{id}/menu -> menu with availability
POST /v1/orders { restaurant_id, items, address, payment_token, idempotency_key } -> { order_id }
GET /v1/orders/{id} -> state, ETA, courier location
PATCH /v1/orders/{id} (restaurant: accept, ready) (courier: picked_up, delivered)
3. High-level design
Separate the services by access pattern:
- Catalogue and search. Restaurants, menus and availability, served from a search index and caches. Read-heavy and eventually consistent.
- Order service. Creates orders and owns the order state machine, backed by a transactional database.
- Payment service. Charges and refunds, as in the payment system chapter.
- Dispatch. Matches couriers to orders using a live geo index.
- Notifications and live updates. Push order status to the customer and restaurant.
The order as a state machine
Three actors change the order, so its state must be explicit and validated.
<!--fig:states-->Each transition is allowed only from specific states and only by the right actor: the restaurant can accept or reject a placed order, the courier can mark picked up only after the order is ready, and so on. Timeouts push stuck orders onward: if the restaurant does not respond in a few minutes, the order is cancelled and refunded, and the customer is told.
4. Potential deep dives
Deep dive 1: How do you make discovery fast and relevant?
The challenge. A customer opens the app and expects nearby, open restaurants with accurate delivery estimates, in under a second.
Weak: query the relational database with distance and filters on every request. A geospatial query joined with menus, hours and availability is expensive, and thousands of them per second overwhelm the database.
Solid: a search index with geo support, fed from the database, plus caching. Index restaurants with their location, cuisine, ratings and opening hours, and serve search and listing queries from it. Cache menus, which change rarely. Keep the database as the source of truth, updated by change capture.
Excellent: a layered read path with a fast-changing overlay. Most restaurant data is slow-moving and cacheable at the edge. The things that change quickly, such as whether a restaurant is currently accepting orders, how busy it is, and the delivery estimate, are served from a small, fast overlay store merged at query time. Compute the estimate from the preparation time, the current backlog at the restaurant and the travel time, and refresh it frequently for the busy ones. Rank by a blend of distance, rating, estimated delivery time and the customer's preferences. Degrade gracefully: if the ranking or overlay service is slow, return the cached order and mark estimates as approximate.
Deep dive 2: How do you place an order correctly?
The challenge. Placing an order touches inventory availability, payment and the restaurant, and any step can fail.
Weak: charge the card, then notify the restaurant, in one request. If notification fails, you have taken money for an order nobody is making. If the request is retried, you may charge twice.
Solid: create the order first, in a pending state, with an idempotency key. Persist the order, then charge with the same key, then move to "placed" and notify the restaurant. A retry returns the existing order.
Excellent: an orchestrated flow with defined outcomes. Create the order and authorise the payment (hold the funds) without capturing. Send it to the restaurant. Capture the payment when the restaurant accepts or when the order is picked up, and release the authorisation if it is rejected or times out. This avoids charging for orders that are never made, and refunds are rarer. Model the flow as a saga with compensations: payment authorised, restaurant accepted, courier assigned, and each failure leads to a defined cancellation. Publish order events through an outbox so that downstream services, such as dispatch and notifications, are driven reliably. Restaurant availability is checked at order time, because the menu cache may be stale, and the final check is the restaurant's acceptance.
Deep dive 3: How do you dispatch couriers?
The challenge. Choose a courier for each order so that the food is delivered hot and couriers are used efficiently.
Weak: assign the nearest courier to each order as it arrives. The courier arrives long before the food is ready and waits, or arrives late. Several nearby orders go to different couriers while one courier could have carried two.
Solid: assign when the food is nearly ready, using travel time. Estimate the preparation time, and dispatch so that the courier arrives at about the moment the order is ready. Rank candidates by travel time to the restaurant, not straight-line distance, from a geo index of available couriers.
Excellent: batching and global optimisation. Collect orders over a short window and solve an assignment problem: which courier takes which order, possibly bundling two orders that share a route, to minimise total late time and courier idle time. Consider the courier's current deliveries, the restaurant's reliability in meeting its estimate, and fairness among couriers. Use the same offer protocol as ride sharing: lock the courier, offer with a timeout, release on decline, and make assignment idempotent. Account for the restaurant's real behaviour: learn per-restaurant preparation time from history, because the stated time is often optimistic.
Deep dive 4: Delivery time estimates
The challenge. The estimate is shown before ordering and updated throughout. Customers judge the service by how close it is.
Weak: a fixed average per restaurant. It ignores current load, traffic and distance.
Solid: a sum of components. Estimate preparation time, courier pickup travel, wait at the restaurant and drop-off travel, each from data.
Excellent: a model with a feedback loop. Predict each component with a model trained on historical deliveries and current conditions: time of day, weather, the restaurant's queue, courier supply nearby. Show a range, not a single number, when uncertainty is high. Re-estimate during the order as events occur (accepted, ready, picked up), and measure the error continuously so you can detect a drift. Be conservative where lateness is costly, because under-promising is better than a missed estimate.
Deep dive 5: Live tracking and status updates
The challenge. Three apps need current state, and the customer wants to see the courier move.
Weak: the apps poll the order endpoint every few seconds. The load multiplies with active orders, and updates are late.
Solid: push order state changes, and stream courier position. Use persistent connections or push notifications for status changes, and stream the courier's position to the customer for the active order only.
Excellent: durable state with live acceleration. Treat the order record as the truth, and the push as an optimisation. Every state change has a sequence number, so an app that missed a push catches up on reconnect. Send the courier position at a modest rate, and let the client interpolate. Use a push notification for key events if the app is closed (accepted, picked up, arriving). Restaurants often use a dedicated tablet, so handle its flaky connectivity with local queues and clear retry behaviour, and escalate to a phone call if an order is not acknowledged.
Deep dive 6: Peak load and failure
The challenge. Dinner time brings the highest traffic, and failures affect real meals.
Weak: provision for average load. Peaks degrade everything.
Solid: autoscale and cache. Scale stateless services on load, cache discovery data, and queue non-urgent work.
Excellent: protect the critical path and degrade the rest. Prioritise the order and dispatch path over browsing and ratings, with separate capacity for each. Shed or simplify non-critical features first (recommendations, promotions). If dispatch is degraded, fall back to simpler matching. If a restaurant is overwhelmed, throttle new orders to it with a "busy" status and a longer estimate, which protects the customer experience. Prepare for dependency failure, especially payments, with timeouts, the unknown-state handling from the payment chapter, and clear customer communication. Plan capacity from the peak hour, and rehearse with load tests before holidays.
5. What is expected at each level
Mid-level. You identify the main services, an orders database, a way to match couriers by location, and a flow for order status. You recognise that browsing and ordering have different needs.
Senior. You build the order state machine with validated transitions and timeouts, handle payment and failure with idempotency and compensation, explain dispatch with preparation time in mind, and design push updates with catch-up. You size from estimates.
Staff. You reason about marketplace dynamics: batching and efficiency, estimate accuracy as a product metric, restaurant and courier behaviour, peak capacity, degraded modes and the data loop that improves estimates. You discuss how you would experiment and measure.
6. Interview questions and model answers
Q: How is this different from ride sharing? There is a preparation phase with its own delay, three parties who must agree on the order state, and the opportunity to batch several orders onto one courier. Dispatch has to time the courier's arrival with the food being ready, not just minimise distance.
Q: How do you avoid charging for an order that is never made? Authorise the payment at order time and capture it when the restaurant accepts or the order is picked up, and release the authorisation if it is rejected or times out.
Q: How do you keep restaurant menus fast and accurate? Cache and index them for browsing, with a small fast overlay for availability and load. Verify at order time and rely on the restaurant's acceptance as the final check.
Q: How do you make delivery estimates accurate? Model the components, preparation, pickup, wait and drop-off, from historical data and live conditions, update them as the order progresses and monitor the error.
Q: What happens if the restaurant never responds? A timeout cancels the order, releases the payment authorisation and notifies the customer, and the restaurant is flagged for follow-up.
Q: How do you handle dinner-time peak? Autoscale the stateless tiers, protect the order and dispatch path with separate capacity, shed non-critical features, throttle orders to overwhelmed restaurants and rehearse with load tests.
7. Common mistakes
- Treating browsing and ordering as one workload.
- Charging before the restaurant has accepted, with no refund path.
- Dispatching the nearest courier at order time, with no regard for preparation time.
- Letting any actor change any state, with no validated transitions.
- Polling for order status instead of pushing with catch-up.
- Using straight-line distance for ranking and estimates.
- No plan for a restaurant that does not respond.