System design deep dive · HLD
Design a Food Delivery Platform (DoorDash/Swiggy-like): full high-level design.
Requirements, back-of-envelope capacity estimation, a high-level design diagram, a deeper deployment architecture diagram, sequence diagrams for order placement and delivery-partner dispatch with live tracking, and an entity-relationship diagram for the data model - with the reasoning an interviewer expects behind every box and arrow.
1. Clarify requirements before drawing any box
Food delivery has three actors with conflicting latency needs at once - a customer waiting on an ETA, a restaurant that needs the order the instant it's placed, and a delivery partner whose location must stream in near-real time. Scope has to separate those explicitly.
Functional requirements
Non-functional requirements
2. Back-of-the-envelope capacity estimation
These numbers decide whether restaurant search can be served from cache, how big the geospatial index for delivery partners needs to be, and why location updates cannot be synchronous database writes.
| Metric | Assumption | Resulting estimate |
|---|---|---|
| Orders placed | 5 million/day, concentrated in 3-hour lunch/dinner windows | ~58 orders/sec average, ~170 orders/sec peak (3x during rush) |
| Active delivery partners | 500,000 online during peak hours | Each posts location every 4s → ~125,000 location writes/sec at peak |
| Restaurant searches | 50 searches per order placed (browsing before deciding) | ~8,500 searches/sec at peak - read-heavy, cacheable per geohash cell |
| Storage per order | ~2 KB (items, modifiers, addresses, status history) | 5M/day × 2 years × 2KB ≈ 7.3 TB |
| Location data | Not persisted per-ping; only latest position matters | ~500K partners × ~150 bytes in an in-memory geo index ≈ 75 MB - trivially cached |
3. High-level design (HLD)
The HLD names the major components and the one-directional data flow between them, without yet committing to region layout, replica counts, or the dispatch algorithm's internals - that belongs in the architecture diagram in the next section.
What each box owns
Order service
Owns the order status state machine (placed → accepted → preparing → picked up → delivered / cancelled), validates the cart against the live menu and item availability, and is the only service allowed to write to the order database. It publishes every status transition as an event rather than letting downstream services poll it.
Dispatch service
Once a restaurant accepts an order, this service queries the geo index for available partners within a radius, scores candidates (distance, current load, idle time), and assigns the order to one - then keeps re-running if the first candidate doesn't accept within a short timeout window. It is deliberately decoupled from the order service so dispatch logic can be tuned independently.
Location service + geo index
Ingests a GPS ping from every online partner every 3-5 seconds and writes only the latest position into an in-memory geospatial store (Redis GEOADD/GEOSEARCH), never into the durable order database. It also fans out position updates to any customer currently tracking a partner assigned to their order, over a WebSocket/long-lived push connection.
Menu/restaurant service, event bus, and push notifications
The menu service is the source of truth for what's orderable right now (a restaurant can 86 an item mid-shift) and is read-heavy, so it sits behind a cache. The event bus decouples the order service from every consumer of its status changes - push notifications, analytics, partner payout accounting - so a slow consumer never blocks the order flow.
4. Detailed architecture diagram
The architecture diagram takes every HLD box and answers "how is this actually deployed?" - in particular, how the system survives a 3x lunch-rush spike and keeps live tracking smooth without overloading the durable database.
| Decision | Choice | Reasoning |
|---|---|---|
| Geographic partitioning | Independent capacity per city/metro cell | Demand is inherently local (a rush in one city doesn't affect another), so scaling per-city avoids over-provisioning globally and contains a hot-city incident. |
| Location storage | In-memory geospatial index (Redis GEO), not the order DB | ~125,000 writes/sec of ephemeral position data would overwhelm a durable relational store; only the latest position ever matters, so TTL-expiring keys are sufficient. |
| Order DB sharding key | Hash of city/region id | Almost every query ("this restaurant's open orders", "this customer's order history") is naturally scoped to one city, avoiding cross-shard joins. |
| Status notifications | Async via Kafka + push workers | The order service's write path must not block on delivering a push notification to a possibly-offline device. |
5. Sequence diagrams for the two critical flows
A sequence diagram is where an interviewer checks whether the design actually separates "the restaurant must confirm before we dispatch" from "the partner's location is a separate, high-frequency stream."
5.1 Place an order against a restaurant's menu
Step 4-5 is synchronous with a short timeout: if the restaurant doesn't respond in time, the order auto-cancels rather than leaving the customer waiting indefinitely. Dispatch (step 6) is only triggered after acceptance, not at order placement, because assigning a partner before the restaurant confirms would waste the partner's time on an order that might get rejected.
5.2 Delivery-partner assignment and live tracking
Step 3 is drawn as a single offer for clarity, but the real flow retries against the next-ranked candidate if the first partner doesn't accept within the timeout window. Steps 5-7 run on an entirely separate, high-frequency loop from the order status updates - location never touches the order database, and a customer's map simply subscribes to whichever partner id is currently assigned to their order.
6. Entity-relationship (ER) diagram and schema
The data model has to answer three questions: how is an order's line-item detail stored without duplicating menu data, how is a delivery partner's current assignment tracked, and why isn't live GPS position a column in any of these tables.
Key modeling decisions
| Storage choice | Use when | Watch out for |
|---|---|---|
| Relational (Postgres/MySQL), sharded by city | Orders and payments need transactional integrity and the query pattern is naturally scoped to one city/region. | Cross-city queries (a partner who happens to move between two metro cells) need application-level stitching. |
| In-memory geospatial (Redis GEO) | Location data is high-frequency, ephemeral, and only the latest value matters. | Not durable by default - acceptable here since a lost position is simply overwritten by the next ping seconds later. |
7. Deep dives interviewers actually probe
How does the dispatch algorithm pick a delivery partner - nearest available, or something smarter?
Pure nearest-partner assignment is simple but greedy - it can leave a partner who just finished a delivery two blocks away idle while a farther one gets sent, and it ignores batching opportunities (one partner picking up two nearby orders from the same restaurant cluster). A practical middle ground scores candidates on a weighted combination of distance, current idle time, and whether the order could be batched with one they're already carrying, re-evaluated on a short interval rather than solved as a one-shot global optimization, which would be too slow to compute per order at this scale.
Why isn't every GPS ping written to the durable order database?
At 500,000 concurrent partners pinging every 4 seconds, that's ~125,000 writes/sec of data where only the single latest value is ever useful - the history of exactly where a partner was 30 seconds ago has no product value. Writing that volume to a relational store would require sharding and scaling infrastructure sized for a workload that's fundamentally disposable; an in-memory geospatial store with a short TTL (so a partner who goes offline silently disappears from search results) is both cheaper and a better fit for the access pattern.
What happens if the restaurant runs out of an item after the order is placed but before it's prepared?
The menu check at order time (step 2 of the placement sequence) reduces but doesn't eliminate this race, since preparation can reveal a stock-out the system didn't know about. The design handles this as a first-class order-service transition - a "partial fulfillment" or "item substitution requested" state that requires a customer decision (accept substitute / remove item / cancel whole order) rather than silently failing, with the restaurant able to trigger it via their partner dashboard at any point before pickup.
How do you keep live tracking smooth without every customer polling the location service?
Customers hold a long-lived WebSocket (or server-sent-events) connection through the WebSocket gateway, and the location service pushes an update only when the assigned partner's position actually changes meaningfully - it never asks the client to poll. Because each customer only cares about exactly one partner's position at a time, the fan-out is naturally narrow (one publisher, one or two subscribers per partner), unlike a broadcast/chat system where fan-out to many subscribers is the hard problem.
What's the single biggest bottleneck during a 3x lunch-rush spike?
Not the geo index or location ingestion, both of which scale horizontally by sharding per city. The real pressure point is the dispatch service's matching loop when supply (idle partners) is temporarily outstripped by demand (orders needing assignment) in one hot neighborhood - the system needs graceful degradation (wider search radius, slightly worse ETAs, surge-adjusted partner incentives) rather than a hard failure, and that logic has to be tested under simulated load, not just designed on paper.
Post a Comment
Add