System design · Orders and money
Design a food ordering and delivery platform
- Reported in several places
- SDE-2 / L5
- 2 diagrams
- 3 sources
Two or more independent public pages, at least one a candidate write-up.
Design an online food ordering and delivery platform where customers search nearby restaurants, place an order with online payment, and track a courier on a map. Assume a dinner-time peak, restaurants that must confirm each order, and couriers who report their location every few seconds.
A representative prompt in the style reported, not a quote from Amazon. The answer below is written for this site.
Ask first
Clarify the problem before you draw anything.
Questions I would ask before drawing anything. Which actors are in scope: customers, restaurants, couriers, or all three? Does the platform employ the couriers or do restaurants deliver themselves? How many cities, and what is the peak order rate? Is payment taken online at order time? Must a restaurant confirm every order, and what should happen if it stays silent? How fresh must "open now" and "sold out" be in search results?
Scope I will assume: search and menu browsing, order placement with online payment, restaurant confirmation, courier assignment, live tracking and status notifications. Out of scope: ratings, loyalty programs, restaurant onboarding, restaurant payouts, fraud scoring.
Numbers
A back-of-envelope that decides the shape.
These are planning figures for the exercise, not facts about any company.
- Orders: 2 million a day is 2,000,000 ÷ 86,400, about 23 a second; plan for a 10x meal-time peak of about 230 a second.
- Order writes: about 10 rows an order, so 230 × 10 = 2,300 writes a second: under the 5,000 one primary handles, with only about 2x headroom.
- Browse reads: 20 million sessions × 20 reads = 400 million a day, 400,000,000 ÷ 86,400 = about 4,600 a second, and a 5x peak is about 23,000. That is past 20,000, so menus and lists come from a cache and a search index.
- Courier pings: 100,000 couriers online at peak, one location every 5 s: 100,000 ÷ 5 = 20,000 writes a second, 4x the 5,000 write line, so locations stay out of the relational store. Latest positions: 100,000 × 100 B = 10 MB.
- Tracking connections: orders in flight = 230 × 2,400 s (40 minutes) = 552,000. At 10 to 50 KB each that is 5.5 to 28 GB; a gateway box holds 250k to 500k connections, so run about 6 for headroom.
The numbers force: partition orders for headroom, cache and index browse, and give courier locations an in-memory path.
The design
Diagrams first, then components, data and the hard parts.
- The app searches; the index plus the live open and sold-out cache answer.
- The client sends the order with an idempotency key; a unique (customer_id, idempotency_key) makes a retry return the same order.
- One transaction inserts the order in CREATED plus an outbox row; a relay publishes order.created to the queue.
- Payment places a hold with the same key; on success the order moves to AWAITING_RESTAURANT.
- Once the order is AWAITING_RESTAURANT, the restaurant gateway pushes it; with no accept in about 3 minutes a delayed job cancels it and releases the hold.
- On accept with a preparation time, dispatch starts searching for a courier.
- Dispatch offers the job to a courier (compare-and-set to offered) and publishes the courier position, which the tracking gateway subscribes to.
- The gateway forwards a position to the app every 10 s; if the stream drops, the app polls.
Open on the whiteboardAll 2 diagrams on one tidy board, with a step-by-step walkthrough.
Services first, then the two paths, then the hard problems.
- Catalog and search. Restaurants and menus live in a relational store (restaurant id, location, opening hours, menu items with price and an
availableflag). Change events feed a search index (inverted index plus a geo filter) that answers "open restaurants that deliver to this point and match this text". Index refresh runs seconds behind, so the live "open" and "sold out" flags come from a cache overlay the restaurant service writes directly. The order service re-checks both at placement. - Order service. Owns the state machine. Tables:
orders(order_id, customer_id, restaurant_id, state, version, total_minor),order_itemswith name and price copied at order time so later menu edits cannot change an old order, and an append-onlyorder_events(order_id, seq, state, at). Shard key iscustomer_id, because "my orders" is the common read. The restaurant's own order list is a read model built from order events. - Payment service authorises (holds) at placement and captures on pickup or delivery.
- Restaurant gateway pushes the order to the restaurant's device and falls back to polling.
- Dispatch and location service keeps the latest courier positions in an in-memory geo store keyed by grid cell.
- Tracking gateway holds the customer connections.
Write path. The client generates an idempotency key and sends the order. A unique constraint on (customer_id, idempotency_key) makes a retry return the same order. The service validates (restaurant open, items available, prices match, address in range) and, in one transaction, inserts the order in state CREATED plus an outbox row. A relay publishes order.created to a queue. The payment service places a hold using the same key and emits the result. On success the order moves to AWAITING_RESTAURANT. If the restaurant does not accept within a window (say 3 minutes) a delayed job cancels the order and releases the hold. Every transition is a conditional update, WHERE state = expected AND version = n, so duplicate or stale events are no-ops. Consumers are idempotent, as in the queues chapter.
Dispatch. When the restaurant accepts with a preparation time, dispatch starts searching so a courier arrives about when the food is ready, not minutes before. Couriers ping every 5 s; each ping overwrites that courier's entry with a 30 s TTL, so an offline courier disappears by itself. A query reads the cell around the restaurant plus its neighbours, ranks candidates by estimated travel time, and offers the job to the best one with a 15 s timeout. To stop two orders offering the same courier, the courier record is changed with a compare-and-set, available to offered, so only one offer can win.
Tracking. The customer app holds an event stream (server-sent events or a WebSocket) to the gateway, which subscribes to the order's courier channel and forwards a position every 10 s. If the stream drops, the app polls the order endpoint.
Hard problem 1: agreement across payment, restaurant and courier without a distributed transaction. Use a saga with an outbox (see the transactions chapter): each step commits locally, each failure has a compensation (void the hold, notify the customer), and timeouts are first-class transitions driven by delayed jobs rather than hope.
Hard problem 2: dinner demand beyond courier supply. Accepting orders nobody can deliver is worse than refusing them. Use admission control per zone and per restaurant: a cap on concurrent open orders, fed by current courier supply, shown to customers as a longer estimate, a higher fee or a "busy" label.
Trade-offs
What you chose, and when you would flip.
- Authorise then capture vs charge at placement. Authorise-then-capture lets you release the money cleanly when a restaurant rejects or a courier is never found, at the cost of a second payment call and hold expiry handling. Charging up front is simpler but turns every cancellation into a refund. Pick authorise-then-capture; flip to charge-up-front when a payment method does not support holds.
- Relational orders vs a key-value store. Order, items and outbox rows commit together, which is what a relational primary gives you for free. Pick relational, sharded by customer. Flip to a key-value store when one shard's writes stay past about 5,000 a second and you have already split by city.
- Central matching vs offers to a courier pool. A dispatcher that offers to the single best courier optimises each trip but takes longer when couriers decline. Offering to the top three at once is faster and wastes offers. Pick one at a time with a short timeout; flip to multi-offer in thin supply, where waiting costs more than a wasted offer.
- Pushed tracking vs polling. Pushing every 10 s costs held connections (552,000 at peak) but feels live. Polling is stateless and cheap to run but multiplies requests. Pick push with polling as fallback; flip to polling only for the pre-pickup stage, when nothing moves.
Follow-ups to expect
Where the interviewer pushes next.
- The restaurant's tablet goes offline right after accepting an order. What does the system do, and what does the customer see?
- A courier's phone loses signal mid-delivery. How do tracking, the estimate and the payout cope?
- Demand doubles in one neighbourhood at 8 p.m. while supply stays flat. What do you change and in what order?
- How would you add scheduled orders for tomorrow's lunch without breaking the peak-time design?
- A customer reports one item missing from a delivered order. How do you model and process the partial refund?
Where candidates slip
The mistakes this question punishes.
- One database for everything. Putting 20,000 location writes a second and 23,000 browse reads a second next to the orders table. The arithmetic rules this out in a minute.
- Skipping the idempotency key. A double tap or a retry after a timeout creates two orders and two charges.
- Treating a restaurant as always responsive. No acceptance timeout means a paid order sits forever.
- Updating menu prices in place. An old order then shows today's price.
- Leaving assignment races unanswered. Two orders offered to one courier, with no compare-and-set.
- Drawing a distributed transaction across payment, restaurant and dispatch instead of a saga with timeouts.
- Never mentioning overload, so supply shortage shows up as late deliveries.
- Polling for courier positions every second from every customer, with no estimate of connection or request counts.
Have a go before you read.
Interviews reward the sketch you make cold. Clarify out loud, draw it, then compare with the answer.
Now make it stick
Where this was reported
Public pages only. Companies do not confirm these, and they rotate their questions, so treat each link as one person's account or one list's tag.