Design problems
Maps, mobility & logisticsHardSDE-2 40 min

Design food delivery (Swiggy / Zomato)

Asked at
BlinkitOlaSwiggyUberZeptoZomato

The interviewer asks

“Design a food delivery platform like Swiggy or Zomato. Customers browse nearby restaurants, place an order and pay; the restaurant accepts and cooks it; a delivery partner is assigned, picks it up and delivers it while the customer tracks it live.”

What a strong answer looks like

A strong answer frames food delivery as a three-sided marketplace (customer, restaurant, delivery partner) held together by an order state machine. Discovery is read-heavy and cacheable; ordering must be correct and idempotent; dispatch and tracking are real-time geo problems. The order service owns a strict state machine (PLACED → PAID → ACCEPTED → PREPARING → PICKED_UP → DELIVERED, with cancel paths) and publishes every transition through an outbox to Kafka. Delivery partners stream GPS every ~5 seconds into a location service keyed by H3 cell. Dispatch does not assign greedily per order; it runs batched matching per zone every few seconds, scoring partner-order pairs by predicted time to restaurant against the food-ready time, and can batch two orders on one trip. ETA is a model of prep time plus travel time and is re-predicted at every stage. Deep dives: the order state machine and payments, batched dispatch, live tracking, and handling the dinner peak and rain.

Concepts to mention

three-sided marketplaceorder state machine with conditional updatesidempotency key on place ordertransactional outbox to Kafkapayment webhook plus reconciliationbatched dispatch with min-cost assignmentorder batching on one trippartner location in Redis by H3 cellETA as prep + first mile + last mileSSE or WebSocket trackinggraceful degradation during rain and peaks

1 Requirements

Functional

  • Customers see restaurants that deliver to their location, with menu, availability and a promised delivery time.
  • Customers build a cart from one restaurant, apply offers, pay (UPI, cards, wallets, cash on delivery) and place an order.
  • Restaurants receive orders on a tablet app, accept or reject, and mark food ready.
  • The system assigns a delivery partner, who accepts, picks up and delivers.
  • Customers track order status and the partner's live location with an updated ETA.
  • Cancellations and refunds per policy; support for order issues.
  • Partners see earnings per trip; restaurants see payouts.

Non-functional

  • Peak load: ~3x average during dinner (8-10 pm), higher on IPL nights, festivals and rain.
  • Place-order API p99 under 1 s excluding the payment provider; no duplicate orders on retry.
  • Order service availability 99.99%; discovery can degrade (stale menus) but must stay up.
  • Partner location to customer screen in under ~5 seconds.
  • Dispatch decides an assignment within ~30-60 seconds of the order being accepted.
  • Work on flaky mobile networks for partners (Tier-2/3 cities, basements of malls).

Out of scope (say it out loud)

  • Restaurant ranking and search details (see restaurant discovery).
  • Pricing of delivery fee and surge in depth (mention only).
  • Partner onboarding, KYC and payout accounting.

2 Estimates

Orders~2M/day; ~25/s avg, ~150/s at dinner peak2M ÷ 86,400 ≈ 23/s; orders cluster into ~6 busy hours with the dinner hour at ~3x the busy-hour average, so plan for ~150/s and 300/s on special days.
Discovery reads~20k/s peak~50M app opens/day with several listing and menu views each; look-to-order ratio ~20:1. Mostly cache hits.
Active partners at peak~300kEach partner does ~1.5-2 orders/hour at peak; 150 orders/s × 3,600 ≈ 540k orders/hour ÷ ~2 ≈ 270k partners.
Partner GPS pings~60k/s300k partners × 1 ping per 5 s = 60k writes/s; tiny payloads (~100 bytes).
Order state events~1,500/s peak~10 state transitions per order × 150 orders/s.
Tracking viewers~500k concurrentOrders in flight at peak ≈ 150/s × ~35 min ≈ 315k; plus restaurant and support viewers.
Order storage~6 GB/day, ~2 TB/year2M orders × ~3 KB (items, addresses, state history).

3 API

EndpointWhat it does
GET/v1/restaurants?lat=&lng=&cursor=Serviceable restaurants with promised ETA; cached by H3 cell.
GET/v1/restaurants/{id}/menuMenu with item availability; CDN/Redis cached with short TTL.
POST/v1/ordersPlace order {cart_id, address_id, payment_method}; Idempotency-Key; returns order_id and payment intent.
POST/v1/restaurant/orders/{id}/acceptRestaurant accepts with prep time estimate; also /reject and /ready.
POST/v1/partner/locationsBatched GPS points from the partner app {[(lat, lng, ts, accuracy)]}.
POST/v1/partner/tasks/{id}/acceptPartner accepts an offered trip; also /picked-up and /delivered (with OTP or photo).
SSE/v1/orders/{id}/trackStream of status, partner location and ETA to the customer app.
POST/v1/webhooks/paymentsPayment provider callback; verified and idempotent.

4 Data model

ordersPostgres sharded by order_id (or Aurora / Vitess)
order_id (PK, time-sortable)customer_idrestaurant_iditems (jsonb)amountstatusversionpartner_idpromised_etacreated_atidempotency_key (unique per customer)

Source of truth. Status changes are conditional updates on (order_id, version) to enforce the state machine. Secondary indexes via read models per customer and restaurant.

order_events / outboxPostgres (same shard) → Kafka
event_idorder_idtypepayloadcreated_at

Written in the same transaction as the status change; a relay or Debezium publishes to Kafka topic orders partitioned by order_id.

restaurants and menusPostgres + Redis + Elasticsearch
restaurant_idlocationh3_cells_servedopen_hoursis_open; items: item_idpricein_stock

Read-heavy. Menus cached in Redis; 'item out of stock' toggles invalidate the cache.

partner_locationRedis
partner:{id} → {latlngtsstatush3_r9}; set per H3 cell of available partners

Latest position only, TTL 2 min. Dispatch reads available partners by cell.

location_trailKafka → Cassandra / S3
(partner_id, day) partitiontslatlng

For distance-based pay, disputes and ETA model training; not on the hot path.

tasks (trips)Postgres
task_idpartner_idorder_ids[]statusoffered_ataccepted_atroute

One trip can carry two orders when batched.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALGPS every 5 sstate + outboxorder eventslatest positionlocation streamready to assignpartners by H3assign partnerstatus + locationCustomer appRestaurant appPartner appAPI gateway + CDNTracking gateway(SSE)Order serviceLocation serviceDiscovery / menu svcKafka (orders,locations)Dispatch engineETA / prep-time modelsPostgres (orders)Redis (menus,locations)Payment gatewayMaps / routing
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Place and pay for an order

  1. 1Customer taps Pay; the app sends POST /orders with an Idempotency-Key generated when the cart was checked out.
  2. 2Order service re-validates the cart against the live menu (prices, stock, restaurant open, address serviceable) and computes the bill.
  3. 3In one transaction it inserts the order with status PAYMENT_PENDING and an outbox row; a retry with the same key returns the same order.
  4. 4It creates a payment intent with the gateway; the customer completes UPI or card authentication.
  5. 5The payment webhook (verified signature, deduped by payment_id) moves the order to PLACED via a conditional update; a reconciliation job polls the gateway for orders stuck in PAYMENT_PENDING.
  6. 6The OrderPlaced event goes from the outbox to Kafka; the restaurant app receives it by push and starts a 3-minute accept timer.

Restaurant accepts and dispatch assigns a partner

  1. 1Restaurant accepts with a prep estimate; the order moves to ACCEPTED and an event is emitted.
  2. 2Dispatch starts looking at the right time, not immediately: the target is a partner arriving when the food is ready (accept time + predicted prep).
  3. 3Every few seconds per zone, dispatch collects unassigned orders and available partners from Redis (by H3 cells around each restaurant).
  4. 4It scores pairs by predicted travel time to the restaurant, wait time, the partner's current trip, and batching opportunities, then solves the assignment for the whole batch.
  5. 5The chosen partner gets an offer; on accept within ~20 s the task is confirmed, otherwise the order returns to the next round with that partner excluded.

Live tracking

  1. 1Partner app sends batched GPS points every 5 s; the location service writes the latest point to Redis and publishes to the Kafka location topic.
  2. 2The tracking gateway consumes order and location events and pushes them over SSE to the customer watching that order (connections sharded by order_id).
  3. 3The ETA service re-predicts remaining time on each stage change and every ~30 s during the ride; the app smooths the marker between points.
  4. 4On delivery the partner enters the customer's OTP; the order moves to DELIVERED and payout and rating flows start from the event.

7 Deep dives

Order state machine, idempotency and payments

The order is a state machine, and most production bugs come from illegal transitions: a cancel arriving after pickup, or a payment webhook arriving after the customer gave up. Store status and a version on the order row and make every transition a conditional update: UPDATE orders SET status='ACCEPTED', version=version+1 WHERE order_id=? AND status='PLACED'. If zero rows change, the transition was illegal or a duplicate, and you return the current state. Write an outbox row in the same transaction so the event is never lost or published without the commit. Place-order uses an Idempotency-Key unique per customer, so a double tap or a retry on a 2G timeout returns the same order instead of charging twice. Payments are asynchronous: UPI collect or intent flows finish seconds or minutes later. Treat the gateway webhook as one input and a reconciliation poller as another; both apply the same idempotent transition keyed on payment_id. If payment succeeds after the order was auto-cancelled, issue an automatic refund instead of resurrecting the order. Long-running steps (accept timer, refund) fit a durable workflow engine like Temporal, keyed by order_id.

Dispatch: batched matching, not nearest partner

Greedy 'nearest free partner when an order arrives' looks fine on a whiteboard and fails in practice. It gives a partner to the first order even if a later order 30 seconds away would be a far better match, and it ignores food readiness: a partner who arrives 15 minutes before the biryani is ready wastes capacity at peak. Strong designs run dispatch in rounds per geographic zone (for example a city split into H3 resolution-7 zones) every few seconds. Each round takes unassigned orders whose food will be ready soon and available or about-to-be-free partners nearby, builds a cost matrix (predicted travel time to the restaurant, idle wait at the restaurant, delay to the customer's promised time, partner fairness), and solves a min-cost assignment (Hungarian algorithm or a heuristic for large zones). Batching two orders from the same or nearby restaurants going in the same direction on one trip raises throughput by 20-30% at peak, constrained so the first customer's ETA does not slip beyond a threshold. Run one dispatch worker per zone, owned via a lease, so decisions for a zone are not made twice. Partners on a 2-minute-from-done trip are counted as available soon, which is how real systems keep wait times low.

Live location and tracking at 60k pings/s

Partners send GPS every ~5 seconds, batched when the network is poor, with client timestamps so late points can be ordered or dropped. The location service is stateless: it validates the point (drops jumps faster than ~120 km/h or with poor accuracy), writes the latest position to Redis with a TTL and updates the partner's H3 cell set when it changes, then appends to a Kafka topic partitioned by partner_id. Dispatch reads Redis; history goes to Cassandra or S3 for pay, disputes and ETA training. For the customer, a tracking gateway holds SSE or WebSocket connections and subscribes to the events for the orders it serves; it pushes status changes immediately and locations at most every few seconds. Only orders in flight are tracked, so ~300-500k viewers is manageable on a few dozen gateway nodes. The app interpolates the marker along the road between points so it does not jump. When the partner is underground or offline, show the last known position and time rather than inventing movement.

ETA: promised time is a product promise

The ETA shown before ordering affects conversion and refunds, so it is a modelled number, not a distance divided by speed. Break it into parts: time to accept, food preparation (a model per restaurant using item count, time of day, current queue of orders at that kitchen), partner first mile (travel to the restaurant plus wait), and last mile (routing with live traffic and historical speeds per road segment and hour). Each part has its own model trained on the location trails and order events; the total is calibrated so that, for example, 90% of orders arrive before the promise. Recompute at each stage and push changes to the customer. Restaurant-side signals matter in India: kitchens get slower during IPL match breaks and festivals, and rain cuts partner speed and supply at the same time. Serve the model from a low-latency feature store (Redis) because listing pages need ETA for every restaurant shown, and cache ETA per (restaurant, H3 cell) for ~1 minute.

8 Trade-offs

Batched assignment vs instant greedy assignment

Greedy assignment is simple and gives the fastest first decision. Batched rounds of a few seconds give much better global matches and allow multi-order trips, at the cost of a small delay and more complex code. Since food is rarely ready in the first minute, the delay is free; choose batched rounds.

One order database vs microservices with their own stores

The order service owns order state; payment, dispatch and restaurant services own their own data and react to events. This avoids distributed transactions: the order saga moves forward or compensates (refund, reassign) on events. The cost is eventual consistency between views, so support tools must read the order service as the truth.

SSE vs WebSocket vs polling for tracking

Tracking is server-to-client only, so SSE over HTTP/2 is simpler and passes proxies well. WebSocket is useful if the same connection carries chat with the partner. Polling every 10 s is an acceptable fallback on poor networks and is easier to cache, but costs more requests.

Allowing cash on delivery

COD is still common in smaller cities. It removes payment failure from the order path but adds fraud and partner cash-handling risk. Gate COD by customer trust score and order value, and reconcile partner cash balances daily.

9 Bottlenecks

Dinner peak and rain

Rain raises orders and cuts partner supply at the same time. The system must shed demand gracefully: increase promised ETAs, add a rain surcharge, temporarily shrink serviceable radius per restaurant, and disable far-away restaurants in listings. These controls must be config-driven per zone so ops teams can act in minutes.

Hot restaurants

A popular outlet can receive hundreds of orders in minutes during a promotion. Its kitchen, not our database, becomes the bottleneck. Track live kitchen load and throttle the restaurant in listings (show 'busy', longer ETA) before orders pile up and get cancelled.

Location write volume at 10x

600k pings/s still fits Redis Cluster and Kafka, but avoid per-ping database writes. Reduce frequency adaptively: every 3 s while riding, every 30 s while waiting at a restaurant, and drop points that moved less than a few metres.

10 Failure handling

Payment gateway is slow or down

Use multiple gateways with health-based routing. Orders stay in PAYMENT_PENDING; webhooks and the reconciliation poller resolve them later. If money is captured after timeout, auto-refund. Never mark an order placed without confirmation from the gateway.

Dispatch worker for a zone crashes

Zone ownership is a lease in etcd or a database; another worker takes over after the lease expires (~10 s). Orders are read from Kafka and current assignment state from the database, so the new owner resumes without double assignment, because assignment is a conditional update on the task row.

Restaurant tablet offline

If the restaurant does not accept within ~3 minutes, escalate with an automated phone call (IVR), then auto-cancel and refund. Repeated offline periods mark the restaurant temporarily closed in discovery.

11 What impresses

  • Times dispatch to food-ready time instead of assigning as soon as the order arrives.
  • Describes batched matching per zone and multi-order trips with an ETA constraint.
  • Handles late payment success after cancellation with automatic refunds.
  • Breaks ETA into components and talks about calibration (e.g. 90% delivered before promise).
  • Names operational levers for rain: radius shrink, ETA inflation, surcharges, per-zone config.
  • Uses a lease per dispatch zone to avoid duplicate decisions.

12 Mistakes

  • Assigning the nearest partner greedily and calling dispatch done.
  • Writing every GPS ping to the orders database.
  • No idempotency on order placement, leading to double charges on retry.
  • Ignoring the restaurant: no accept/reject flow or prep time.
  • Using distributed transactions across payment, order and dispatch instead of events and compensation.
  • Showing a fixed '30 minutes' ETA with no model.

13 Follow-ups

Q1How do you prevent two dispatch rounds from offering the same partner two orders?
Q2How would you implement order batching without hurting the first customer?
Q3A partner's GPS is spoofed to look closer to restaurants. How do you detect it?
Q4How do you scale to a 10x spike during an IPL final?
Q5What happens if Kafka is down for 5 minutes?
Q6How would you add scheduled orders for later in the day?

Practice questions

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free