Design Zomato (Food Delivery)
Combines geospatial restaurant search, an order state machine, and delivery partner matching (similar to Uber) — all under sharp lunch/dinner peak load.
1. Requirements
- · Search restaurants by location, cuisine, rating
- · Browse menu, place an order
- · Real-time order status tracking (placed → preparing → picked up → delivered)
- · Assign a delivery partner and track their live location
- · Payments and post-delivery ratings/reviews
- · 50M monthly active users
- · 5M orders/day, heavily skewed to lunch (1-2pm) and dinner (8-10pm)
- · 200K restaurant partners, 1M delivery partners
- · Peak QPS can be 5-10x average during meal windows
2. Estimation
3. Core Challenges
"Restaurants near me, open now, serving biryani, rating > 4" combines geospatial filtering (radius search) with text/facet search (cuisine, rating). Precompute a geo-index (same H3/geohash approach as Uber) intersected with a search index (Elasticsearch) for facets.
Same geospatial matching problem as Uber's driver matching, but the optimal assignment must jointly consider restaurant pickup location AND customer drop location, not just one pickup point.
Unlike a simple CRUD flow, an order moves through strict states (placed → accepted_by_restaurant → preparing → ready_for_pickup → picked_up → out_for_delivery → delivered / cancelled) with multiple actors (customer, restaurant, delivery partner) each triggering transitions. Illegal transitions (e.g. marking "delivered" before "picked up") must be rejected atomically, and every transition needs to notify the other two parties in real time.
4. Architecture
5. Database Choices & Rationale
| Data | Storage | Why |
|---|---|---|
| Orders (state machine) | PostgreSQL / MySQL | Needs ACID transactions — order status, payment, and inventory decrement must be atomic and consistent. |
| Restaurant/menu catalog | PostgreSQL + Elasticsearch | Source of truth in SQL; denormalized into Elasticsearch for fast faceted search. |
| Live delivery partner locations | Redis (GEO, in-memory) | Same as Uber — sub-ms writes at 250K updates/sec, only latest position matters. |
| Restaurant geo-index | Redis GEO / H3 grid | Fast radius queries for 'restaurants near me' before hitting the search index. |
| Order events / audit trail | Kafka + Cassandra (append-only) | High write volume, time-series, used for tracking history and analytics, not live queries. |
| Ratings & reviews | PostgreSQL + Redis cache for aggregates | Relational at write time; cached average ratings for fast read on restaurant listing pages. |
6. Bottlenecks & Solutions
Auto-scale order/search services ahead of known peak windows (predictable, scheduled scaling rather than reactive-only). Queue order placement through Kafka to smooth bursts into the restaurant acceptance flow instead of overwhelming restaurant-side systems directly.
Restaurant-side order throttling: expose a 'max concurrent orders' cap per restaurant; once hit, temporarily mark it unavailable for new orders (auto-managed via a live queue-depth counter) rather than accepting orders it can't fulfill in time.
Expand search radius progressively (ring expansion) if no partner found in the innermost ring within a timeout. Surge/incentive pricing for delivery partners to pull more partners into high-demand zones, mirroring Uber's surge mechanism.
Every state transition is an idempotent, durable Kafka event (not just an in-memory push) — if a websocket push fails, the client can always re-fetch current state via a REST fallback endpoint that reads the authoritative Order DB.
7. Key Trade-offs Summary
| Decision | Option A | Option B | Chosen & Why |
|---|---|---|---|
| Order state storage | NoSQL (eventual) | SQL (ACID) | SQL — payment-linked state transitions need strong consistency, no ambiguity |
| Restaurant search | SQL LIKE query | Geo-index + Elasticsearch | Geo-index + ES — radius + facet search at low latency, scales with catalog size |
| Delivery matching | Global optimal assignment | Greedy nearest + ring expansion | Greedy — near-real-time match needed, small ETA loss is acceptable |
| Order events delivery | DB polling by clients | Kafka + WebSocket push | Push — lower latency, less load than clients repeatedly polling |
| Peak load handling | Reactive auto-scaling only | Scheduled + reactive scaling | Both — lunch/dinner peaks are predictable, pre-warm capacity ahead of time |
| Partner location storage | Durable DB | In-memory (Redis) | Redis — same reasoning as Uber, only latest position needed, huge write QPS |
8. Follow-up Questions
A: Menu/inventory availability is best-effort at order time (not a hard reservation, since kitchens don't track exact real-time stock like a warehouse). On accept, restaurant can flag specific items unavailable; Order Service triggers a partial-refund + customer notification flow rather than blocking the whole order.
A: Combine: (a) restaurant prep-time estimate (historical average per restaurant/item, refined by current queue depth), (b) routing-service travel time estimate from restaurant to customer, and (c) time to find/dispatch a delivery partner. Recompute and push updated ETA at each state transition rather than showing a single static estimate.
A: Same core idea as Uber: track live demand (open orders) vs supply (available delivery partners) per geo-cell, recomputed on a fixed interval, and apply a multiplier to delivery fees to both offset the imbalance for customers and pull more partners into that zone via incentive pay.
A: Partner status transitions use the same atomic compare-and-swap approach as Uber — 'available' -> 'assigned' via a Lua script or row lock, so only one matching request can win. Multi-order batching (when intentional, for nearby restaurants) is handled explicitly as a distinct assignment type, not an accidental double-booking.