Design Uber Eats
You're asked to design a food-delivery app like Uber Eats. The prompt covers several systems at once: restaurant search, ordering, payments, driver matching, and delivery tracking. You cannot design all of them in 45 minutes, so let's break the product into those parts and agree on a scope.
Clarify the requirements
Start by listing the major subsystems: restaurant and menu management, search and discovery, ordering, payments, driver matching, and delivery tracking. Clarify which ones are in scope. For this breakdown, we'll focus on three common choices: searching for restaurants and food, driver matching, and customer payments. Assume that menu entry and restaurant order acceptance already exist, and limit the geography to the US.
The functional requirements are:
- Customers can search for restaurants and dishes.
- Customers can place an order and pay for it.
- Drivers receive delivery assignments when an order is ready for pickup.
The non-functional requirements differ by flow. Search should respond within a few hundred milliseconds and remain highly available. It can tolerate eventual consistency within a stated freshness limit: a menu edit appearing after a minute is acceptable, but showing a restaurant as open an hour after it closes is not. Driver matching and payments need strong consistency because double-charging a card or assigning two drivers to one order has immediate consequences. Assign consistency requirements per flow and explain why they differ.
High-level architecture
Use a set of independently scalable services behind a load balancer:
- API gateway. Routes customer traffic to the service that owns each surface.
- Search service. Reads from a search index kept current by a reindex pipeline off the restaurant database.
- Order service. Owns the order record and its state transitions.
- Payment service. Called synchronously, because the order can't proceed until the charge succeeds.
- Event broker. Carries
OrderPaidand later status events to everything that reacts to them. - Delivery service. Holds live driver connections and runs matching, detailed in deep dive 3.
As you draw the architecture, explain these three storage choices:
Restaurant and menu data goes in a horizontally scalable NoSQL store. The data is read-heavy, doesn't need cross-row transactions, and partitions naturally by restaurant or region.
Orders go in a SQL database. An order is a small record that moves through states (pending → paid → preparing → ready → delivering → delivered), money is attached, and ACID transactions are exactly what you want for status transitions.
Driver locations need a purpose-built store, covered in the driver-matching deep dive. Thousands of moving phones send a steady stream of small writes that the other two stores should not have to absorb.
Deep dive 1: search, and the freshness trade-off
Querying the restaurant database directly with LIKE filters may work at first, but it does not scale well or support relevance ranking, typo tolerance, and geographic filtering.
Use a dedicated search index such as Elasticsearch. The restaurant and menu database remains the source of truth, while change data capture or periodic reindexing pushes updates into the index. Search queries read from the index, but user-facing requests never write to it directly.
Next, decide how fresh the index must be. If a restaurant closes or changes its prices, the index stays stale until the next sync. Frequent reindexing costs more compute, while infrequent updates risk showing closed restaurants or old prices.
Set freshness by field because stale data does not carry the same risk everywhere. Open status and delivery availability should update within seconds or a minute, so send them through a fast CDC path or verify them at order time against the source of truth. Menu descriptions and photos can lag by minutes or hours through batch reindexing. You can also tune the reindex interval for each restaurant based on how often its menu changes.
Always validate at order time. Search may show slightly stale results, but the Order Service should check the restaurant's status and prices against the primary database before accepting the order. Stale browse data is inconvenient; stale data in the payment path can charge the wrong amount.
Deep dive 2: ordering and payments
Ordering is where the event-driven and pub/sub pattern becomes useful, and where the boundary between synchronous and asynchronous work matters most.
Build versus buy for payments. Processing cards yourself brings PCI compliance, bank integrations, and audit obligations. Unless payments are the product, use a processor such as Stripe and focus the design on the orchestration around it. State that boundary so the scope is clear.
The synchronous core. The order-placement path must be synchronous through payment. You cannot tell a restaurant to start cooking on a payment that might still fail:
Idempotency and the source of the key. Mobile networks retry requests. If the app times out and submits an order again, the customer must not be charged twice. Generate one idempotency key on the client for each logical order attempt. A server-generated key would make the retry look like a new order. The Order Service stores the key with the order and passes it to the payment processor. A duplicate submission then finds the existing record and returns the same result instead of creating another charge. This applies the at-least-once delivery technique from event-driven and pub/sub at the API boundary.
The asynchronous fan-out. After payment, an OrderPaid event fans out to the restaurant's order feed, customer notifications, analytics, and fraud review. Keep those consumers outside the checkout request so their failures do not break checkout. Write an outbox row in the same transaction as the status update to avoid the dual-write problem. Every paid order will eventually publish an event, and no event can refer to an order that rolled back.
Deep dive 3: driver matching
For driver matching, answer three questions: where the drivers are, how to choose one, and how to prevent two drivers from claiming the same order.
Tracking driver locations. Drivers on shift hold a persistent connection (WebSockets fit well; this edge of the system borrows from the real-time pattern) and report location to a driver location store. Only on-shift drivers live in the store, which immediately cuts the write volume.
Two optimizations keep this manageable at scale. First, shard the store geographically. A Seattle order does not need data about drivers in New York. Partitioning by geohash cell or region limits each matching query to the restaurant's area. Second, throttle location writes on the client. The phone can report only after the driver moves a meaningful distance instead of sending an update every second. This cuts write volume by an order of magnitude without reducing matching quality.
Selecting a driver. When the restaurant marks an order ready, an OrderReady event reaches the Delivery Service. It queries the local geographic shard for nearby available drivers and ranks them. Distance provides a baseline, while ETA, current heading, and opportunities to batch pickups can refine the score. Treat the ranking function as a black box and spend your time on the surrounding systems. Designing the scoring model is a separate ML problem.
Guaranteeing one driver per order. Two matchers running concurrently, or a driver accepting just as the offer times out, can double-assign. The fix is an assignment store with an atomic claim: a table keyed by order_id where assigning a driver is a conditional write ("set driver_id if currently unassigned"). The same mechanic prevents one driver from carrying two orders if the product forbids it, via a reverse index keyed by driver_id, at the cost of a second write per assignment. This is a compare-and-set lease, the same primitive job systems use to ensure one worker per job in async jobs and workers.
If a driver cancels mid-delivery, clear the assignment row and re-enter matching. Because the claim is atomic, a concurrent match can't slip in a second driver during the handoff.
Follow-up questions to expect
Be ready for follow-up questions like these:
- How do you keep search results fresh when menus change? Segment freshness by field, CDC for critical fields, and validate at order time.
- What happens if the payment succeeds but your service crashes before recording it? The idempotency key makes the retry safe: re-submitting reconciles against the processor rather than double-charging.
- How do you avoid charging twice on a client retry? Client-generated idempotency key, stored with the order, passed to the processor.
- Why not update driver locations every second? Client-side significant-movement filtering; the store is write-heavy enough as it is.
- How does matching stay fast with 100,000 drivers online? Geo-sharding bounds every query to a small candidate set.
- Two matchers pick different drivers for the same order. Now what? They can't both win: assignment is a conditional write on the order row.
Leveling signals
Related lessons
The same publish-and-react shape, aimed at customer endpoints that may be slow or offline.
Where the synchronous money path and the async fan-out meet, with compensation on failure.