The system design question
Design a food delivery app like DoorDash. Customers browse restaurants and place orders, restaurants accept them, a courier is assigned, and the customer can follow the order until it arrives.
Agree on the scope (5 minutes)
Say up front what you're covering.
| We'll design | We'll skip |
|---|---|
| Browsing nearby restaurants and their menus | Search ranking and recommendations |
| Placing an order and paying | Courier pay and tips |
| The restaurant accepting it | Ratings and reviews |
| Assigning a courier | Refunds and customer support |
| Following the order until it arrives |
Then agree on what "good" means:
- Orders never get lost: once the customer is charged, the order has to reach the restaurant and end up delivered or refunded.
- Live status: the customer sees each step, like 'accepted' or 'picked up', within a few seconds, and the courier's dot moves on the map.
- Quick courier match: a courier is offered the order within a minute or so of the restaurant accepting it.
- Fast browsing: the list of nearby restaurants loads in a few hundred milliseconds.
Rough numbers (5 minutes)
These are assumptions: 20 million orders a day, about 80% of them in a 4-hour window around lunch and dinner, 1 million couriers online at a busy time sending their location every 5 seconds, and each order checked on by the customer about 20 times. Meta's design round is often 45 minutes, so get through these quickly.
- ~1K
- orders a second at dinner
- ~200K
- courier location updates a second
- ~5K
- status checks a second
- ~10 KB
- per order, all in
| Question | Rough math | What it means |
|---|---|---|
| How many orders at a peak? | 20 million × 80% ÷ (4 hours × 3,600) | About 1,100 a second. Small for a database, but every one involves money |
| How many location updates? | 1 million couriers ÷ 5 seconds | About 200K writes a second. That's the biggest load, and none of it needs to be kept forever |
| How often do customers check? | 20 million × 20 checks ÷ 86,400, more at dinner | A few thousand a second. Easy, if we push updates rather than have phones poll |
| How much order data? | 20 million × 10 KB a day | About 200 GB a day. A sharded SQL database handles it fine |
The overall design (10 minutes)
- The customer opens the app and a restaurant service returns places nearby that are open and deliver to them, using a map index of restaurant locations.
- They place an order. The order service saves it, charges the card through a payment provider, and moves it to 'sent to restaurant'.
- The restaurant's tablet gets the order and accepts it with a prep time. That's a state change, saved and published as an event.
- Courier matching hears 'accepted', looks up couriers near the restaurant, and offers the job to one. When they accept, the order gets that courier.
- Couriers send their location every few seconds. A connection service pushes status changes and the courier's position to the customer's app until the order is delivered.
What the apps need to do
| Action | Who does it | What's sent |
|---|---|---|
| List nearby restaurants | The customer app | Their location, plus a cursor for the next page |
| Get a menu | The customer app | Restaurant ID |
| Place an order | The customer app | Restaurant, items, delivery address, payment method and an idempotency key |
| Accept or reject an order | The restaurant tablet | Order ID and a prep time |
| Accept a delivery offer | The courier app | Offer ID |
| Update status | The courier app | Order ID and 'picked up' or 'delivered' |
| Send my location | The courier app | Courier ID, position and time, every few seconds |
| Follow my order | The customer app | Order ID. Opens a connection for live updates |
What we store and where
| Data | Where it goes | Why |
|---|---|---|
| Orders and their items | SQL database, split by order ID | Money and state changes need transactions |
| Order history of state changes | Same database, one row per change | So you can see what happened, and replay it |
| Restaurants and menus | SQL database with a cache in front | Changes rarely, read constantly |
| Restaurant locations | Map index by geohash or H3 cell | So 'what's near me' is a quick lookup |
| Courier locations | In-memory store, by map cell, overwritten every few seconds | Huge write volume, only the latest point matters |
| Order events | Event stream, split by order ID | Drives matching, notifications and live updates |
Step 1: The order's states (12 minutes)
Write the order down as a state machine. Each step only goes forward, and only certain people can move it: placed, paid, sent to restaurant, accepted, courier assigned, picked up, delivered. Plus a few side exits like rejected and cancelled.
The order service is the only thing that changes an order's state. A change is one database update that checks the current state first, like 'move to picked up, but only if it's courier assigned right now'. If two things race, one wins and the other gets a clear no. A SQL database is a good fit, because you want that check and the write to happen together.
Every change also writes an event. Save the event in the same transaction as the state change, then a small worker sends saved events onto the event stream. That way you never change an order without telling anyone, or tell everyone about a change that didn't save.
Paying without charging twice
Phones retry when the network drops, so 'place order' can arrive twice. The app sends an idempotency key with it, and the order service returns the same order if it's seen that key before. Send the same key on to the payment provider too.
Authorize the card when the order's placed and only capture the money once the restaurant accepts. If the restaurant says no, release the hold and the customer's never charged.
Step 2: Finding a courier (10 minutes)
Couriers send their location every 5 seconds. Keep only the latest point, in memory, grouped by map cell. This is geospatial indexing: to find couriers near a restaurant, look in its cell and the cells around it.
When an order's accepted, the matcher finds free couriers nearby and scores them on how far they are from the restaurant, and whether they'll get there around when the food's ready. There's no point sending someone who arrives 20 minutes before the food does.
Offer the job to the best courier for about 30 seconds. If they say no or don't answer, offer it to the next one. While an offer's out, that courier is marked busy so they don't get two at once.
- Start matching a bit before the food's ready, using the prep time the restaurant gave, so the courier arrives as it's being bagged.
- If nobody takes it after a few tries, widen the search and tell the customer it's taking longer.
Step 3: Live updates to the customer (8 minutes)
Polling every few seconds from millions of phones is wasteful. Instead the app opens a long-lived connection, like a WebSocket, to a connection service. That service keeps a map of which order each open connection is following.
It reads the order events and the courier's location updates and pushes them to whoever's following that order. A push notification covers the big moments, like 'your courier is here', for people who've closed the app.
The connection can drop at any time. When the app reconnects, it first fetches the order's current state from the order service, then listens for changes. So a missed message never leaves the screen wrong for long.
- Send the courier's position to the customer every few seconds at most. Their map can smooth the movement in between.
- Status changes should feel instant. Location can be a few seconds behind.
Choices you should be able to defend
| We chose | Instead of | Because | We'd reconsider if |
|---|---|---|---|
| SQL for orders | A NoSQL store | Transactions make state changes and payments safe | Order volume grew past what sharding SQL could handle comfortably |
| Courier locations in memory only | Saving every point to a database | 200K writes a second of data that's stale in 5 seconds | We needed the full route for disputes, so we'd also log points to cheap storage |
| Offering one courier at a time | Broadcasting to several and taking whoever's first | Couriers don't race for jobs and get let down | Matching was too slow in busy areas |
| Pushing updates over a connection | The app polling for status | Fewer requests and updates arrive faster | Most customers closed the app right after ordering |
What could go wrong (5 minutes)
| What happens | What we do about it |
|---|---|
| The restaurant's tablet is offline | If they haven't accepted in a few minutes, call the restaurant or cancel and release the payment hold |
| The payment provider times out | Retry with the same idempotency key. If it's still unclear, check with the provider before charging again |
| A courier's phone stops sending locations | Their last position goes stale. Stop offering them new jobs, and alert support if they're mid-delivery |
| The connection service restarts | Apps reconnect, fetch the current state, then carry on listening |
| A courier cancels after pickup was due | Move the order back to 'needs a courier' and match again, starting near the restaurant |
Follow-up questions (5 minutes)
How would you show an arrival time?
Add up the prep time left, the courier's travel to the restaurant and from there to the customer. Start with map travel times, then adjust with past deliveries in that area.
Could one courier carry two orders?
Yes, if the restaurants are close and the drop-offs are on the way. The matcher looks for an order that fits a courier's current route before picking a free courier.
What happens at a huge dinner rush?
Orders are fine, the database barely notices. The squeeze is couriers. Show longer wait times, and keep matching close by before widening the search.
How the 60 minutes add up
| Minutes | What you're doing |
|---|---|
| 0–5 | Agree on what you're building |
| 5–10 | Rough numbers |
| 10–20 | Draw the overall design |
| 20–32 | The order's states |
| 32–42 | Finding a courier |
| 42–50 | Live updates to the customer |
| 50–55 | What could go wrong |
| 55–60 | Follow-up questions |
Key concepts used here
- Relational vs non-relational databasesPick a database based on how you'll actually use it.
- Message queues and event streamsKeep services from depending on each other, and handle traffic spikes.
- Idempotency and retriesMake sure a retried request doesn't happen twice.
- Geospatial indexingFind what's nearby with map grids like geohashes and H3.
- Caching strategiesMake reads faster, and figure out what to do when data changes.
- Sharding and consistent hashingSplit data across machines and keep it balanced.
- Real-time deliveryPush updates to users the moment they happen, with WebSockets and friends.
Further reading
We wrote this sample answer ourselves.
Practice Loop is independent and isn't affiliated with or endorsed by Meta.