System Design · Marketplace Deep Dive

Design Swiggy / Zomato: Food Delivery System Design

A three-sided marketplace with a 40-minute promise: how to design discovery, ordering, rider dispatch, live tracking and ETAs that survive the 8 PM dinner rush.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 18 min read
Isometric city at night with delivery scooters moving between restaurants and homes, coordinated by a central dispatch hub
TL;DR
  • Food delivery is a three-sided, hyperlocal, real-time marketplace: customers, restaurants and delivery partners, all within a few kilometres, all moving within a 30–45 minute window.
  • Split the system into discovery (read-heavy, cacheable, geo-filtered), ordering (transactional, an explicit state machine), and logistics (rider location, dispatch, tracking, ETA).
  • Rider assignment works best as batched matching per zone every few seconds, timed so the rider arrives when the food is ready, not as "nearest free rider wins".
  • Live tracking is a write-heavy location stream: keep only the latest position hot (Redis/geo index), push updates to watchers over WebSocket/SSE, archive history asynchronously.
  • The dinner peak is predictable. Pre-scale, cache per geo-cell, isolate checkout from browsing, and use serviceability controls (radius, surge fee) as a business-level load shedder.

"Design Swiggy" sounds like a CRUD app with a map. It isn't. The hard parts are the ones you can't see in the app: deciding which of 40 nearby riders should pick up an order that will be ready in 11 minutes, keeping a customer's map moving smoothly while a million riders send GPS pings, and promising "32 mins" with a straight face at 8:30 PM on a rainy Saturday. This guide is structured around those hard parts, with the plain parts kept brief.

Why Food Delivery Is Different from Ride-Hailing and E-commerce

Candidates often reuse their Uber answer. There is overlap (location, matching), but three differences change the design:

DimensionRide-hailing (Uber/Ola)E-commerce (Flipkart)Food delivery (Swiggy/Zomato)
SidesRider, driverBuyer, seller, warehouseCustomer, restaurant, delivery partner
When to dispatchImmediatelyHours later, plannedWhen food will be ready minus rider travel time
PerishabilityNoneLowHigh: cold food is a failed order
Demand shapeCommute peaksSale eventsSharp lunch and dinner peaks every single day
BatchingPooling, optionalCoreValuable: two orders from one restaurant, same direction

The preparation time of the restaurant is the variable that makes this problem unique. Assign a rider too early and they wait at the restaurant (wasted fleet capacity at peak). Too late and the food sits on the counter.

Requirements

Functional

  • Customer: browse/search restaurants and dishes deliverable to their location, see ETA and fees, place an order, pay, track live, rate.
  • Restaurant: receive orders, accept/reject, mark "food ready", toggle availability and menu items.
  • Delivery partner: go online/offline, receive assignment offers, accept, navigate, mark picked up and delivered, see earnings.
  • Platform: assign riders, compute ETAs, handle cancellations and refunds.

Non-functional

  • Browse latency under ~200 ms at p99; it is the top of the funnel.
  • Order placement and payment must be correct and durable (no lost or duplicate orders).
  • Rider location freshness within a few seconds; tracking map updates every few seconds.
  • High availability through the lunch and dinner peaks; graceful degradation when the fleet is saturated.

Out of scope (say so): restaurant onboarding, ads, grocery/quick commerce, rider payouts.

Capacity Estimates

Use round, clearly assumed numbers. Real company figures are not public at this granularity, so state your assumptions:

  • 3 million orders/day across India; 40% of them in a 3-hour dinner window.
  • Dinner order rate: 1.2M / 10,800 s ≈ 110 orders/s average, with the peak minute maybe 3x that: ~350 orders/s.
  • Browse-to-order ratio ~20:1 on sessions, ~10 API calls per session → 1.2M × 20 × 10 / 10,800 ≈ 22,000 read requests/s at dinner, more at the peak minute.
  • 300,000 riders online at peak, pinging every 4 seconds → 75,000 location writes/s.
  • Each ping ~100 bytes → 7.5 MB/s; over a day with ~12 active hours on average, roughly 300 GB/day of raw location history. Hot store only needs the latest point: 300K × 100 B = 30 MB. Tiny.
  • Active orders being tracked at peak: if an order lasts ~35 min, 350/s × 2,100 s ≈ 735,000 concurrent tracked orders.
~350/s
Peak order writes
~75K/s
Rider GPS pings
~22K+/s
Browse reads
~700K
Concurrent live-tracked orders

The insight: orders are a small, high-value write stream; location is a large, low-value write stream; browsing is a large read stream. They need different storage and scaling strategies, which is the backbone of the architecture.

APIs

Key endpoints
# Customer
GET  /v1/feed?lat=12.9716&lng=77.5946&cursor=...      → restaurants serviceable here
GET  /v1/search?q=biryani&lat=..&lng=..&veg=true
GET  /v1/restaurants/{id}/menu
POST /v1/cart/quote        { restaurant_id, items[], address_id } → price, fees, ETA, quote_id
POST /v1/orders            { quote_id, payment_method }  Idempotency-Key: ...
GET  /v1/orders/{id}
WS   /v1/orders/{id}/track  → stream of {status, rider_lat, rider_lng, eta_min}

# Restaurant
POST /v1/restaurant/orders/{id}/accept   { prep_time_min: 15 }
POST /v1/restaurant/orders/{id}/ready

# Delivery partner
POST /v1/rider/location    { lat, lng, speed, heading, ts, battery }   (batched)
POST /v1/rider/offers/{offer_id}/accept
POST /v1/rider/orders/{id}/picked_up
POST /v1/rider/orders/{id}/delivered     { otp }

The quote step matters: price, delivery fee and ETA are computed once, given a short-lived quote_id, and the order is placed against that quote. This prevents the "fee changed between cart and checkout" race and gives you a natural place to apply surge pricing.

High-Level Architecture

Customer app Restaurant app/tablet Rider app | | | v v v +--------------------------------------------------------+ | API Gateway / Edge (auth, rate limit) | +----+-----------+-------------+--------------+----------+ | | | | v v v v +---------+ +---------+ +-----------+ +--------------+ |Discovery| | Cart & | | Order | | Location | |& Search | | Pricing | | Service | | Ingestion | +----+----+ +----+----+ +-----+-----+ +------+-------+ | | | | v v v v +---------+ +---------+ +-----------+ +--------------+ | Elastic | | Redis | | Orders DB | | Redis GEO / | | search | | quotes | | (sharded | | rider state | | + cache | | | | SQL) | +------+-------+ +---------+ +---------+ +-----+-----+ | | events | pings v v +----------------------------+ | Kafka event bus | +--+--------+--------+-------+ | | | v v v +---------+ +--------+ +-----------+ |Dispatch | | ETA | | Tracking | |(matcher)| |service | | fan-out | +---------+ +--------+ | (WS/SSE) | +-----------+

Each box maps to one of the three streams from the estimates: Discovery handles reads, Order Service handles the transactional writes, and Location Ingestion plus Tracking handle the GPS firehose. Kafka decouples them so a slow consumer (say, analytics) never blocks order placement.

Serviceability first, ranking second

A restaurant appears in your feed only if it is serviceable for your address right now: open, within its delivery radius or polygon, and the zone has enough riders. Serviceability is computed per geo-cell, not per user. Divide the city into cells (geohash precision 6 is about 1.2 km × 0.6 km, or H3 resolution 8 hexagons, roughly 0.7 km² each) and precompute, every minute or so:

serviceable:{cell_id} → [restaurant_id, base_eta_min, delivery_fee] (sorted)

Now the feed request is: map the user's lat/lng to a cell, fetch the precomputed list from cache, apply personalisation (past orders, cuisine affinity, offers) and return a page. This turns an expensive geo query into a cache lookup, which is the main reason browse scales. The geo-indexing options are compared in geohash vs quadtree vs S2/H3.

Search

Search for "paneer biryani" spans restaurant names, cuisines and dish names. Use Elasticsearch/OpenSearch with one document per restaurant containing nested or flattened dishes, plus a geo_shape for its delivery area:

Elasticsearch query (simplified)
{
  "query": {
    "bool": {
      "must": [{ "multi_match": {
          "query": "paneer biryani",
          "fields": ["dish_names^3", "name^2", "cuisines"],
          "fuzziness": "AUTO" } }],
      "filter": [
        { "term": { "is_open": true } },
        { "term": { "is_veg": true } },
        { "geo_shape": { "delivery_area": {
            "shape": { "type": "point", "coordinates": [77.5946, 12.9716] },
            "relation": "contains" } } }
      ]
    }
  }
}

Note GeoJSON order is [lng, lat], a classic bug. Handle Indian-language and spelling variation ("biriyani", "biryani") with synonyms and fuzziness. Real-time fields like is_open and item availability change often; update them via partial updates from a Kafka stream, and re-check availability at quote time anyway, because the search index is eventually consistent.

Menu reads

Menus are read millions of times and changed rarely. Cache the full menu JSON per restaurant in Redis/CDN with a version number; restaurant edits bump the version. Item out-of-stock toggles are a small overlay fetched separately so you don't invalidate the whole menu at peak.

Deep Dive: The Order Lifecycle

The order is the system's contract with three parties, so model it as an explicit state machine owned by one service:

PLACED --pay ok--> CONFIRMED --restaurant accepts--> PREPARING | | | | pay fail | restaurant rejects / timeout | rider assigned v v v PAYMENT_FAILED CANCELLED (auto refund) RIDER_ASSIGNED | rider at restaurant v RIDER_AT_PICKUP | food handed over v PICKED_UP | v DELIVERED

Note that "rider assigned" and "preparing" happen in parallel in reality; model rider assignment as a separate sub-state or a separate delivery entity linked to the order. Many teams split Order (what the customer bought) from Delivery/Trip (how it reaches them), which keeps reassignment of riders from touching order state.

Placing an order safely

  1. Validate the quote (not expired, items still available, restaurant still open).
  2. Create the order in PLACED with the client's idempotency key under a unique constraint, so double-taps on "Place order" create one order.
  3. Initiate payment. For UPI the result is asynchronous; the order waits in PLACED until a payment success event arrives (the payment flow is covered in payment system design). Cash on delivery skips straight to confirmed.
  4. Notify the restaurant via a persistent connection to the restaurant tablet (and an IVR/phone fallback if not acknowledged within a minute or two).
  5. Restaurant accepts with a prep time; this is the most important input to dispatch.

Timeouts as first-class events

Every state has a deadline: restaurant must accept within N seconds, rider must accept an offer within ~15–30 seconds, rider must reach pickup by T. Use a delay queue or a timer service (see distributed job scheduler) that fires a "deadline" event; the order service checks whether the state already moved on and otherwise escalates (reassign, call the restaurant, cancel and refund).

Deep Dive: Delivery Partner Assignment

This is where you can stand out. The naive answer, "find the nearest free rider with a geo query", has three problems: the nearest rider might be better used for another order a minute later, the rider might arrive 15 minutes before the food is ready, and it ignores batching.

When to dispatch

Target rider arrival at restaurant ≈ food ready time. If the restaurant says 15 minutes and riders in the area are typically 6 minutes away, start looking around minute 7–9, not at minute 0. In practice platforms start somewhat earlier at peak because finding a rider takes time and offers get declined.

Batched matching

Every few seconds, per zone, the dispatcher takes the set of orders that are due for assignment and the set of candidate riders (free, or finishing a delivery nearby, or already carrying a compatible order), builds a cost matrix and solves an assignment.

Cost function (illustrative)
cost(rider, order) =
      w1 * max(0, rider_arrival_at_restaurant - food_ready_time)   # food waits
    + w2 * max(0, food_ready_time - rider_arrival_at_restaurant)   # rider waits
    + w3 * predicted_delivery_time_to_customer
    + w4 * (1 if rider would exceed shift / max orders else 0) * BIG
    - w5 * batching_bonus(rider.current_order, order)

With N orders and M riders in a zone, the Hungarian algorithm solves the optimal one-to-one assignment in O(n³), fine for zones of a few hundred. For larger zones, or when batching makes it not one-to-one, a greedy approach over sorted (rider, order) pairs is a robust approximation. Here is a compact, runnable greedy version:

Python
import heapq

def assign(orders, riders, travel_min, now_min):
    """orders: {oid: ready_at_min}; riders: list of rider ids;
    travel_min(rid, oid) -> minutes for rider to reach the order's restaurant."""
    heap = []
    for oid, ready_at in orders.items():
        for rid in riders:
            arrive = now_min + travel_min(rid, oid)
            food_waits = max(0, arrive - ready_at)
            rider_waits = max(0, ready_at - arrive)
            cost = 3.0 * food_waits + 1.0 * rider_waits
            heapq.heappush(heap, (cost, oid, rid))
    assigned_o, assigned_r, result = set(), set(), {}
    while heap and len(result) < min(len(orders), len(riders)):
        cost, oid, rid = heapq.heappop(heap)
        if oid in assigned_o or rid in assigned_r:
            continue
        result[oid] = rid
        assigned_o.add(oid); assigned_r.add(rid)
    return result

# Dry run
dist = {("r1","o1"): 4, ("r1","o2"): 9, ("r2","o1"): 3, ("r2","o2"): 5}
print(assign({"o1": 10, "o2": 6}, ["r1","r2"],
             lambda r, o: dist[(r, o)], now_min=0))
# {'o2': 'r2', 'o1': 'r1'}

Dry run: o2 is ready at minute 6, o1 at minute 10. Pair costs: (r2,o2) arrives at 5, rider waits 1 → cost 1. (r1,o1) arrives at 4, rider waits 6 → cost 6. (r2,o1) arrives 3, waits 7 → 7. (r1,o2) arrives 9, food waits 3 → 9. Greedy picks (r2,o2) then (r1,o1). Nearest-rider-first would have given o1 to r2 (3 minutes away) and left o2's food waiting 3 minutes for r1. Batched matching avoided that.

Offers and concurrency

  • An assignment becomes an offer pushed to the rider app. The rider accepts or it times out; on decline/timeout the order returns to the next matching round with that rider excluded.
  • A rider must never hold two conflicting offers. Lock rider state with an atomic compare-and-set in Redis (SET rider:{id}:offer {order} NX EX 30) before sending an offer.
  • Run one dispatcher instance per zone (partition zones across instances, e.g. via Kafka partitions keyed by zone) so matching within a zone is single-threaded and lock-free. Zone boundaries cause edge effects; allow riders near a boundary to be candidates in both and resolve with the CAS lock.

Batching (stacked orders)

Two orders from the same restaurant, or from restaurants a few hundred metres apart, going in the same direction can share a rider. Batch only when the added delay for the first customer is under a threshold (a few minutes) and the food can still arrive hot. Batching is an enormous efficiency lever at peak when riders are the bottleneck.

Compare this with immediate dispatch in Uber/Ola system design; the location index is the same, the timing problem is not.

Deep Dive: Live Order Tracking

Ingestion

The rider app collects GPS every 2–4 seconds and sends pings in small batches (every 4–10 seconds) to save battery and data, over HTTP/2 or a persistent connection. Ingestion is stateless: validate, drop obviously bad points (accuracy > 100 m, impossible speed), and:

  • Write the latest position to Redis: GEOADD riders:{city} lng lat rider_id plus a hash with heading, speed, timestamp and status. This is the index dispatch queries.
  • Publish the ping to Kafka topic rider-locations, partitioned by rider ID, for tracking fan-out, ETA updates and archival to a cheap store (S3/columnar) for model training and dispute resolution.

Expire stale riders: if no ping in 60 seconds, mark them as unreachable for dispatch rather than letting a phone that died in a basement receive offers.

Fan-out to customers

Only riders with an active order have watchers, and usually one watcher (the customer, sometimes also a support agent). The tracking service consumes rider-locations, looks up rider → active order → subscribed connections, and pushes an update. A customer connects with a WebSocket or SSE to any tracking gateway node; the gateway registers order_id → gateway_node in Redis so the tracking consumer can route messages to the right node (or use Redis pub/sub channels per order). Trade-offs between transports are discussed in WebSockets vs SSE vs long polling; for a one-way stream, SSE is enough and simpler through proxies.

Throttle updates per watcher to one every 3–5 seconds and let the client interpolate the marker along the route polyline. Users perceive smooth movement; you send 5x fewer messages. When the app is backgrounded, drop the stream and fall back to push notifications at state changes only.

Map matching

Raw GPS in dense Indian cities jumps across buildings and flyovers. Snap points to the road network (map matching, often a hidden Markov model over candidate road segments) before showing them, or at least smooth them. This also improves ETA, since you know which road the rider is on.

Deep Dive: ETA Prediction

The ETA shown in the feed, at checkout and during tracking is the product's most visible promise. Decompose it:

ComponentKey featuresNotes
Food preparation timeRestaurant, dishes, current open orders at the restaurant, time of day, historical accuracy of the restaurant's own estimatesRestaurants' self-reported prep times are noisy; learn a correction per restaurant
Rider to restaurantRider availability in the zone, distance, road travel timeAt peak, include expected time to find a rider
Wait at pickupRestaurant's historical handover delay, parking, mall vs street-frontMalls and food courts add minutes
Last mileRoute distance, traffic for the hour, weather, building type (gated society, high-rise)Gated societies add walk and security time

Models are typically gradient-boosted trees or neural nets trained on historical orders, with predictions served from a low-latency feature store. Two engineering points interviewers like:

  • Predict distributions, show a conservative quantile. Showing the median means half the orders are late; companies tune which percentile to show to balance conversion against lateness.
  • Recompute ETA on events (restaurant accepted, food ready, picked up) and periodically from location, but smooth what you show so the number doesn't jump around. Users hate "25 min" becoming "38 min" without explanation.

Road travel time itself comes from a routing engine over the road graph with time-dependent edge weights, as in Google Maps system design.

Surviving the 8 PM Dinner Peak

Unlike a flash sale, the dinner peak happens every day and is highly predictable. That changes the playbook from "react" to "prepare":

  1. Scheduled pre-scaling. Scale stateless services (API, discovery, tracking gateways) ahead of 7 PM based on forecast, not just CPU autoscaling, which lags sudden ramps.
  2. Cache the feed per geo-cell with short TTLs (30–60 s). 22K reads/s become a few thousand cache keys per city.
  3. Bulkhead the critical path. Separate pools and databases for checkout/order/payment versus browse/recommendations. If recommendations time out, show a non-personalised list; checkout keeps working.
  4. Serviceability as load shedding. When the rider supply in a zone drops below demand, shrink the delivery radius, raise delivery fees, show longer ETAs, or temporarily mark distant restaurants unserviceable. This sheds load at the business level, which is far better than letting orders in and delivering them cold in 90 minutes.
  5. Shard orders by city/zone so a hot city (Bengaluru on a Friday) cannot affect others, and dispatch is naturally partitioned.
  6. Protect restaurants. A restaurant with 30 open orders will be late. Throttle its visibility or extend its prep time automatically.
  7. Weather events. Rain in Mumbai spikes demand and cuts rider supply at the same time. Feed weather into ETA and serviceability, and pre-announce surge fees.
💡
Say this in the interview"The real bottleneck at peak isn't my servers, it's the rider fleet. So my load shedding is mostly business controls (radius, fees, ETA honesty), and my servers just need to stay up behind them."

Data Stores Summary

DataStoreWhy
Orders, payments, refundsSharded PostgreSQL/MySQL (by city or order ID)Transactions, state machine updates with conditions, idempotency
Restaurants, menus (source of truth)SQLRelational, low write rate
Search indexElasticsearch/OpenSearchFull text, fuzzy, geo-shape filters
Feed per cell, menus, quotesRedis / CDNHot reads, short TTLs
Rider latest location and stateRedis GEO + hashes, sharded by city75K writes/s, radius queries
Location historyKafka → object storage / CassandraAppend-heavy, analytics and disputes
Events between servicesKafkaDecoupling, replay
ETA featuresFeature store (online Redis + offline warehouse)Low-latency model inference

Interview Tips and Common Mistakes

How to pace a 45-minute round

  • 5 min requirements; explicitly name the three sides and the three data streams.
  • 5 min estimates; highlight that location writes dominate.
  • 10 min architecture and order state machine.
  • 15–20 min on one or two deep dives the interviewer picks: assignment, tracking or ETA. Offer them; don't wait to be asked.
  • 5 min on peak handling and failure modes.

Follow-up questions to expect

  • "A rider's phone dies mid-delivery. What happens?" Pings stop; after a timeout, flag the trip, attempt to call the rider, and if unreachable either reassign (if not picked up) or escalate to support with the last known location.
  • "Restaurant rejects after payment." Order to CANCELLED, automatic refund via an idempotent refund request, suggest similar restaurants.
  • "How do you avoid two riders getting the same order?" Order-level assignment is a conditional update (WHERE rider_id IS NULL) and rider-level offers use CAS locks.
  • "How would you A/B test a new dispatch algorithm?" Randomising per order contaminates the shared rider pool; use switchback experiments (alternate algorithm by zone and time window).

Mistakes I see in mock interviews

  • Spending 15 minutes on menu CRUD and user login.
  • Assigning the nearest rider immediately at order time, ignoring prep time.
  • Storing every GPS ping in the orders database.
  • Using a relational geo query per feed request instead of precomputing serviceability per cell.
  • No mention of timeouts in the order lifecycle (restaurant never responds, rider never accepts).
  • Showing a single ETA number with no discussion of how it is predicted or updated.

Get the three streams, the order state machine and a batched, prep-time-aware dispatcher right, and the rest of the design follows. For more location-heavy practice, try designing a proximity service next.

Frequently Asked Questions

How does Swiggy assign delivery partners to orders?▾
Publicly described approaches use batched matching: instead of assigning each order the moment it arrives, the dispatcher collects orders and available riders in a zone every few seconds and solves an assignment problem that minimises total delay, considering food preparation time, rider distance and possible order batching. The exact production algorithms are proprietary.
How is live order tracking implemented in food delivery apps?▾
The rider app sends GPS pings every few seconds to a location ingestion service, which stores the latest position in an in-memory store keyed by rider. Customers watching an order get updates over a WebSocket or server-sent events stream, or by polling every few seconds, and the client interpolates movement on the map.
What database does a food delivery system use?▾
Usually several: a relational database for orders and payments where transactions matter, a search engine like Elasticsearch for restaurant and dish discovery with geo filters, Redis for rider locations and hot state, and a wide-column or time-series store for location history and analytics.
How is delivery ETA calculated?▾
ETA is the sum of predicted components: time until the restaurant finishes preparing food, time for a rider to reach the restaurant, waiting time at pickup, and travel time to the customer. Each component is predicted by an ML model trained on historical orders, using features like restaurant load, time of day, weather and road network travel times.
How do food delivery apps handle the dinner rush?▾
They pre-scale before predictable peaks, cache restaurant listings per area, degrade non-critical features, throttle serviceability by shrinking delivery radius or adding surge fees when riders are scarce, and keep the order and payment path isolated from browse traffic so heavy browsing cannot take down checkout.
Is Swiggy system design asked in Indian interviews?▾
Yes, food delivery is a common system design prompt at Indian product companies because interviewers and candidates both understand the product. Expect follow-ups on rider assignment, tracking at scale and order state consistency rather than on the restaurant menu CRUD.

Crack System Design at Swiggy, Zomato & Flipkart

Prepflix's system design program drills marketplace problems like food delivery with live mock interviews and detailed feedback.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.