HV
home / system-design / zomato

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

Functional
  • · 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
Scale Assumptions
  • · 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

Average order QPS: 5M orders/day / 86,400 ≈ 58 QPS average Peak order QPS (lunch/dinner windows, ~4 hours/day carry ~60% of orders): 3M orders / (4 x 3600 sec) ≈ 210 QPS peak (3.5x average) Restaurant search QPS (read-heavy, users browse before ordering): Read:Write ratio ~20:1 ≈ 1,160 search QPS average, ~4,000 QPS peak Delivery partner location updates (same pattern as Uber): 1M delivery partners / 4 sec ping interval ≈ 250,000 updates/sec Storage (order records, ~2KB with items/pricing): 5M x 2KB x 365 ≈ 3.6 TB/year

3. Core Challenges

Restaurant Search (Geo + Text)

"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.

✓ Geo-index narrows candidates fast; Elasticsearch ranks/filters within that set
Delivery Partner Assignment

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.

✓ Reuse Uber-style H3/geohash grid + ring expansion for candidate partners
~ Batch assignment (assign multiple nearby orders to one partner for multi-pickup) adds complexity
The Order State Machine Is the Real Complexity

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

Search flow: Client -> GET /restaurants?lat,lng,cuisine,minRating -> Search Service: 1. geo-index lookup (H3 cell + neighbors) -> candidate restaurant_ids 2. Elasticsearch query (cuisine, rating, open-now) scoped to candidates 3. rank by relevance + distance -> return top N Order flow: Client -> POST /order {items, restaurant_id, address} -> Order Service creates order (status=PLACED), writes to Order DB -> Notification to restaurant (push/websocket) -> Restaurant accepts -> status=ACCEPTED -> Kafka event "order_accepted" -> Matching Service (Uber-style) assigns nearest available delivery partner considering restaurant pickup point -> status=PARTNER_ASSIGNED -> Restaurant marks ready -> status=READY_FOR_PICKUP -> Partner picks up -> status=PICKED_UP -> live location streamed to customer -> Partner delivers -> status=DELIVERED -> payment settled, rating prompt sent Every status transition: - validated against allowed state machine transitions (reject otherwise) - published to Kafka "order_events" topic - fanned out via WebSocket/push to customer, restaurant, and partner apps Delivery partner location (identical pattern to Uber): App pings every 4s -> Location Service -> Redis GEO index (in-memory, latest position only)

5. Database Choices & Rationale

DataStorageWhy
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

⚠️ Lunch/dinner rush causes 3-10x normal load in a tight 1-2 hour window (thundering herd)
→

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.

⚠️ A popular restaurant in a dense area gets flooded with simultaneous orders, exceeding kitchen capacity
→

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.

⚠️ No delivery partners available nearby during peak hours (supply-demand imbalance)
→

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.

⚠️ Order status update lost/delayed, customer sees stale tracking info
→

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

DecisionOption AOption BChosen & 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

Q: How would you handle a restaurant running out of an item after the customer already ordered it?

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.

Q: How do you estimate delivery ETA shown to the customer?

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.

Q: How would you design surge/dynamic delivery fees?

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.

Q: How do you prevent a delivery partner from being assigned to two far-apart orders at once?

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.