Online Food Delivery
Food Ordering and Delivery Platform
Design the object model of a food delivery platform: a customer orders from a restaurant, the restaurant accepts and cooks, a dispatcher hands the order to one free driver, and every party sees the order move through a fixed set of states.
Overview
Three parties take part in one order. The customer picks items from a restaurant menu and pays. The restaurant accepts or rejects the order, cooks it and marks it ready. A driver collects it and delivers it. The platform in the middle owns the order, decides which driver gets it, enforces which status changes are legal, and tells everyone when something changes.
This is a design exercise, not a description of how any real company runs. Interviewers pick it because it has a clean state machine (the order), a pluggable decision (which driver), a rule table (what a cancellation refunds), and a race (two orders must not get the same driver). A strong answer names the entities in the first minutes, draws the order states before writing code, puts the driver choice behind a strategy interface, and shows where the lock lives.
Keep money in whole cents and keep the order's status changes in one method, so a bad transition is impossible rather than merely unlikely.
Scoping the problem
- Delivery only, or also pickup and scheduled orders? — Each adds states and skips others — Delivery now; say how pickup would drop the driver states
- When is a driver assigned: when the restaurant accepts, or when the food is ready? — Early assignment wastes driver time; late assignment lets food go cold — Assign on accept, with a range limit; discuss the trade-off
- Who can cancel, and when? — The refund rule depends on how far the kitchen has gone — The customer, up to the moment the food is ready
- One restaurant per order? — Multi-restaurant orders need several pickups per trip — One restaurant
- Is payment in scope? — Payment is its own state machine — Record the total; treat charging as a call to another service
- How many drivers at once? — Decides whether a list scan is fine or you need a spatial index — Hundreds per city in the interview; say what changes at 100,000
The order lifecycle
Every order walks the same path. The restaurant can reject it once, at the start. The customer can cancel until the food is ready. After pickup the only way out is delivery.
The transitions live in one dictionary, keyed by the current status, and Order.move_to is the only method that changes status. That gives three things at once: an illegal jump such as Ready to Delivered raises, the history list is always complete, and every listener is told about every change from one place.
One order, end to end
Validation happens before the order object exists. A closed restaurant, an unavailable item or an empty basket raises ValueError and nothing is stored, so there is never a half-made order to clean up.
Choosing the driver
The dispatcher asks a DispatchPolicy to rank the free drivers and takes the first one inside the range limit. Distance is the great-circle distance from the haversine formula with the Earth's mean radius of 6,371 km. With a restaurant at 37.7749, -122.4194 and three free drivers, the ranking is a plain sort.
- Ana — 0.01 degrees north — about 1.11 km — yes — 1
- Ben — 0.02 degrees west — about 1.76 km — yes — 2
- Cy — 0.05 degrees north — about 5.56 km — no — skipped
The two policies in the code show why the interface pays off. NearestDriver sorts by distance. FewestDeliveries sorts by completed deliveries and uses distance to break ties, which spreads the work across the fleet. The dispatcher does not know which one it holds.
- Nearest driver — distance — short pickup times — busy drivers near popular restaurants get everything
- Fewest deliveries — (deliveries, distance) — fair earnings — a farther driver may be sent
- Both, weighted — a score mixing the two — most real fleets — needs tuning and explanation
When no driver is in range
An accepted order with no driver is not an error. It goes on a waiting list, and every time a driver is released the platform runs retry_waiting, which tries the list again. The order keeps cooking; only the driver is missing.
Cancellation refunds
The refund is a rule table keyed by the status at the moment of cancellation, not a chain of ifs. Reading the table tells you the policy; changing the policy changes one line.
- Placed — all — all — nothing has happened yet
- Accepted — all — all — the kitchen has not started
- Preparing — none — all — ingredients are used; no delivery happened
- Ready, Picked up, Delivered — cancellation refused — cancellation refused — the food exists and is on its way
An order of two pizzas and a soda (3,100 cents of food plus a 299 cent fee) refunds 3,399 cents if canceled while Placed and 299 cents while Preparing. Those two numbers are asserted in the demo.
Thread safety
Two accepted orders can call assign at the same moment. Without a lock both see the same free driver and both take her. The dispatcher therefore does the whole choice under one threading.Lock: list the free drivers, rank them, mark the winner busy. Python's docs describe Lock as the lowest-level synchronization primitive: one thread holds it, the others block, and the with statement releases it even if the body raises. The demo starts eight threads against one free driver and asserts that exactly one order gets her and seven wait.
What you will be able to do
- Draw the order state machine and enforce it with one transitions table and one method.
- Put the driver choice behind a strategy interface and show two policies that differ in one sort key.
- Express cancellation refunds as a rule table and compute them in cents.
- Explain where the lock lives, what race it prevents, and prove it with a threaded demo.
- Say how the design changes for 100,000 drivers: a spatial index instead of a list scan, and a database transaction instead of an in-process lock.
Functional requirements
- Place an order from one open restaurant: every item must exist and be available, quantities positive, total in cents
- The restaurant accepts or rejects a placed order; an accepted order is offered to one free driver in range
- Move an order through Placed, Accepted, Preparing, Ready, PickedUp and Delivered, refusing any other move
- The customer can cancel until the food is ready; the refund follows a rule table keyed by status
- An accepted order with no driver waits and is retried whenever a driver is released
- Anyone can subscribe to an order and is told about every status change
Non-functional requirements
- A driver is never assigned to two live orders: the choice runs under one lock
- The dispatch policy is swappable without touching the dispatcher or the orders
- Money is integer cents; no floats in prices, totals or refunds
- Validation happens before an order is stored, so a failed request leaves no state behind
- Every status change is recorded in the order history and pushed to subscribers from one place
Interview questions
- Should the driver be assigned when the restaurant accepts or when the food is ready?
- How do cancellation rules stay easy to change?
- Nearest-driver search scans every driver. What do you do with 100,000 drivers?
- How do the customer, restaurant and driver see status changes in real time?
- How would you persist this, and what happens when the driver app retries a request?
Sign up free to read the full topic
More Low-Level Design topics
Start free on Cariara · Coding problems · System design studio