SkillPilotAI

Case Study 6: Uber / Swiggy Ride and Food Delivery

Match a customer to the nearest driver/delivery partner using real-time location tracking, and provide ETA and surge pricing.

C.1 Requirements

Functional requirements:

  • The driver/delivery partner sends their live location every 4-5 seconds.
  • A rider requests a ride or a customer places a food order.
  • Find the nearest available driver and send a ride/order offer (accept/reject).
  • Show the ETA and fare estimate for pickup and drop.
  • Apply surge pricing when demand is high and supply is low.
  • Order/trip state machine: requested -> assigned -> picked up -> delivered/completed.
  • Live tracking on a map, payment and rating.

Non-functional requirements:

  • Matching latency < 2-3 seconds, location update ingestion < 1 second.
  • Strong consistency for trip/order and payment: one driver must not be assigned to two rides.
  • Eventual consistency is OK for location data (a 2-5 sec old location is fine).
  • Availability 99.99%: it must work at peaks too (lunch/dinner, rain, New Year).
  • Scale: 50 lakh (5 million) online drivers/partners, peak 1 lakh+ new orders per minute.
  • City-wise isolation: one city's issue must not bring down another city.

C.2 Estimation (back-of-the-envelope)

ItemNumberHow it is derived
Online drivers/partners (peak)50 lakhAssumption: India + global combined scale.
Location update QPS~12 lakh/sec50 lakh drivers / 4 sec interval = 12.5 lakh writes/sec; not in a DB but in an in-memory geo index.
Location payload bandwidth~125 MB/sec ingress12.5 lakh x ~100 bytes (driver_id, lat, lng, ts, heading) = 125 MB/sec.
Orders/rides per day2 croreAvg 230/sec, peak (dinner) 5x = ~1,200/sec; 10-20 geo queries per order.
Geo index memory~1 GB50 lakh x ~200 bytes (location + status + cell id) = 1 GB; fits easily in a Redis cluster, small even with replicas.
Trip history storage~1 TB/day GPS trace2 crore trips x 30 min x a point every 4 sec (450 points) x 100 bytes = ~900 GB/day; compressed in cold storage.
Order DB storage~20 GB/day2 crore orders x ~1 KB = 20 GB/day, ~7 TB/year; sharded SQL is enough.

C.3 API design

EndpointPurpose
POST /v1/drivers/locationBatch update of the driver location (also possible over a WebSocket/gRPC stream)
POST /v1/rides/estimateFare and ETA estimate for pickup/drop, with the surge multiplier
POST /v1/ridesCreate a ride request or order; with an Idempotency-Key header so no double booking happens
POST /v1/rides/{id}/acceptThe driver accepts the offer; with a conditional update only one driver wins
PATCH /v1/rides/{id}/statusState transition: arrived, picked_up, completed, cancelled
WS /v1/rides/{id}/trackPush the driver's live location to the rider (WebSocket)

C.4 Data model

Table / StoreFieldsWhy
driver_locations (Redis GEO / H3 cell sets)key cell:{h3_id} -> set of driver_ids; driver:{id} -> lat, lng, status, updated_at (TTL 30 sec)Lakhs of updates and nearby queries every second; an in-memory geo index is fast, and the TTL removes offline drivers automatically.
trips / orders (MySQL/Postgres, sharded by city_id ya order_id)trip_id PK, rider_id, driver_id, status, pickup, drop, fare, surge, version, created_atThe state machine and payment need ACID and conditional updates.
trip_events (Kafka -> Cassandra)trip_id, event_type, ts, payloadAn audit log of every state change; event sourcing and analytics.
location_history (Cassandra, partition key trip_id)trip_id, ts, lat, lngA write-heavy GPS trace; for fare disputes and route replay.
surge_multipliers (Redis)key surge:{city}:{h3_cell} -> multiplier, expires_atRead on every estimate, recomputed every 1-2 min.
drivers (SQL)driver_id, vehicle_type, rating, city_id, kyc_statusProfile data rarely changes; relational.

C.5 High-level architecture

Uber / Swiggy: architecture and request flow
  1. 1.Clients: Driver / Partner App (GPS every 4 s), Rider / Customer App (book, track), Restaurant App (accept, prep time)
  2. 2.Load Balancer -> WebSocket Gateway (persistent conn, live track, offers push) and API Gateway (auth, rate limit, Idempotency-Key)
  3. 3.Services: Location Service (lat/lng -> H3 cell), Matching / Dispatch (k-ring drivers, offer), ETA Service (road graph + traffic), Trip / Order Service (state machine, version), Pricing & Surge (base + distance + surge)
  4. 4.Data: Redis Geo Index (city-wise shards, cell:{h3} drivers, driver:{id} TTL 30 s), Map / Routing Engine (OSM road graph), sharded MySQL (trips, drivers), Redis surge (surge:{city}:{cell}) fed by the Surge Calculator (Flink, 1-2 min window)
  5. 5.Kafka: location + trip events, partitioned by city / trip_id
  6. 6.Consumers (idempotent, retry safe): Notification Worker (push, SMS), Payment Worker (capture, payout), Analytics / ML (ETA correction), Cassandra (location_history, trip_events)

Location: 50 lakh drivers / 4 s = ~12.5 lakh writes/s, not in a DB: in-memory geo index ~1 GB + TTL. Ride: 2 cr/day, peak ~1,200/s, 10-20 geo queries per order, ~20 GB/day in SQL.

  • Location path: Driver app -> LB -> WebSocket Gateway -> Location Service -> Redis Geo Index (cell set + TTL) -> Kafka.
  • Ride path: Rider -> API Gateway -> Trip Service creates a row with status = requested in MySQL -> Dispatch takes the k-ring drivers from Redis.
  • Dispatch ranks with the ETA Service + Map/Routing and sends the offer to the driver via the WS Gateway; on accept, a conditional UPDATE trips.
  • Kafka events: the Flink surge calculator writes the multiplier into Redis surge; also Cassandra history, notification and payment workers.
  • The 12.5 lakh location writes/s go not to a DB but to a city-wise sharded Redis geo index; trip and payment stay in strongly consistent SQL.
ComponentWhat it does
Location ServiceTakes driver location updates over WebSocket/gRPC, updates the Redis geo index (H3/geohash cell) and streams to Kafka.
Matching / Dispatch ServicePicks available drivers from the cells around the pickup, ranks them by ETA and sends the offer; Uber publicly uses the H3 hexagonal grid.
Trip / Order ServiceRuns the order state machine; every transition is a conditional update (version check) so invalid or duplicate transitions cannot happen.
ETA ServiceTravel time from the road graph + live traffic + historical data; in food delivery the restaurant prep time is added too.
Pricing & SurgeBase fare + distance + time; a Flink stream job computes the surge multiplier from every cell's demand/supply ratio.
WebSocket GatewayPersistent connections for riders and drivers; pushes live tracking and ride offers.
Notification & Payment WorkersSend push notifications, SMS and do payment capture idempotently from Kafka events.

C.6 Main flows

Driver location update:

  1. The driver app sends the GPS location every 4 sec over a WebSocket/gRPC stream.
  2. The Location Service converts lat/lng into an H3 cell id.
  3. If the cell changed, remove the driver from the old cell set and add them to the new one (Redis).
  4. Latest location and status in the driver:{id} hash, TTL refreshed to 30 sec.
  5. The event goes to Kafka: for the surge calculator, active trip tracking and history.
  6. If the driver has an active trip, the WebSocket Gateway pushes the new location to the rider.

Ride request and driver matching:

  1. After seeing the estimate, the rider sends POST /v1/rides with an Idempotency-Key.
  2. The Trip Service creates the trip row with status = requested.
  3. Dispatch gets the available drivers from the pickup H3 cell and the surrounding rings (k-ring).
  4. It gets the actual road ETA of the top candidates from the ETA Service and ranks them (not straight-line distance).
  5. The offer is pushed to the best driver with a 10-15 sec timeout; on reject/timeout, the next driver.
  6. The driver accepts: UPDATE trips SET driver_id=?, status='assigned' WHERE id=? AND status='requested'.
  7. At the same time the driver's status in Redis becomes busy (with a lock) so no other offer reaches them.
  8. The rider gets the driver details and live ETA over the WebSocket.

Food order state machine:

  1. The customer places the order and payment is authorized; status = placed.
  2. The order goes to the restaurant app; on accept, status = accepted, with a prep time estimate.
  3. Dispatch assigns the nearest partner slightly before the prep time ends so the food does not get cold.
  4. The partner reaches the restaurant -> arrived, takes the food -> picked_up.
  5. Live tracking reaches the customer over a WebSocket; the ETA is recomputed on every update.
  6. On delivery status = delivered, with payment capture and partner payout through a Kafka worker.
  7. Every transition is validated: for example you cannot go back from delivered to picked_up.

C.7 Deep dives

Driver location update every 4 sec: H3 cell -> Redis -> Kafka
  1. 1.Driver App: GPS, WS/gRPC batch
  2. 2.Location Service: lat/lng -> H3 cell id
  3. 3.Redis Geo Index: cell changed? SREM old, SADD new; driver:{id} HSET, TTL 30 s
  4. 4.Kafka location events -> Surge Calculator (Flink), History -> Cassandra / S3, Active trip -> WS push to rider
  5. 5.Surge: in every H3 cell demand requests / supply free drivers, 1-2 min window -> multiplier -> surge:{city}:{cell} Redis, cap on sudden jumps

If the TTL is not refreshed the driver is offline and drops out of the set automatically.

Matching: k-ring -> ETA rank -> offer -> lock
  1. 1.Check only drivers in the pickup cell + the k=1 ring
  2. 2.Candidates D1, D2, D3: from Redis cell sets, status = free
  3. 3.ETA Service ranking: road ETA D2 4 min, D1 6 min
  4. 4.Offer to D2 for 10-15 s; reject / timeout -> next D1
  5. 5.Redis: SET lock:D2 NX TTL; only one wins (Ride A dispatch, D2 accepts; Ride B dispatch also offers to D2)
  6. 6.UPDATE trips SET driver_id=D2, status='assigned' WHERE id=A AND status='requested' -> 1 row = win; Ride B: lock fail / 0 rows -> next driver D1
Food order state machine
  1. 1.placed -> payment authorize
  2. 2.accepted: restaurant, prep time
  3. 3.assigned: partner, before prep finishes; cancelled possible
  4. 4.arrived: at the restaurant
  5. 5.picked_up: live tracking over WS
  6. 6.delivered: capture + payout via Kafka

Every transition is a conditional update (version check) + idempotent; delivered -> picked_up is rejected.

Problem: finding the nearest driver efficiently (among lakhs of drivers). Calculating distance over the whole table is slow. Use geo indexing: geohash or H3 hexagonal cells. Check only the drivers in the pickup cell + neighbour rings, then rank by road ETA.

Problem: 12 lakh/sec location writes. Do not write every update to a persistent DB. Use a city-wise sharded Redis geo index with a TTL; history goes in batches from Kafka to Cassandra/S3. Client-side batching, and a lower frequency when the driver is not moving.

Problem: one driver gets assigned to two riders (race condition). A short-lived distributed lock on the driver (Redis SET NX with TTL) plus a conditional update/optimistic locking (version) on the trip row. Only one transaction wins; the others get the next driver.

Problem: surge pricing in real time. Ride requests (demand) and available drivers (supply) stream on Kafka; Flink computes the ratio for every H3 cell in a 1-2 min window and puts the multiplier in Redis. Caps smooth out sudden jumps.

Problem: accurate ETA. A routing engine on a road graph (OpenStreetMap-like), live traffic speed per road segment, historical patterns and ML correction. For food, also model the restaurant prep time and parking time.

Problem: a flaky network: duplicate requests and lost updates. An Idempotency-Key on every write API; idempotent state machine transitions; on a WebSocket reconnect the client fetches the last known state; idempotent Kafka consumers.

C.8 Trade-offs

DecisionWhat was chosen and why
Geohash vs H3 vs QuadTreeChose H3: all neighbours of a hexagon are at equal distance, the same cells serve both surge and matching; geohash is simple but distorts at the edges.
Redis vs DB for live locationRedis: volatile data, lakhs of writes/sec, only the latest value is needed; durability comes from Kafka + Cassandra history.
Polling vs WebSocket for trackingWebSocket: lower latency and overhead; but sticky connections and gateway scaling must be handled.
Greedy nearest vs batch matchingGreedy is fast and simple; batch matching (global optimum within a window of a few seconds) gives a better overall ETA; companies like Uber use batched dispatch.
Strong vs eventual consistencyTrip state and payment are strong (SQL transactions); location and surge are eventual: speed matters more.

C.9 Interview tips

  • Calculate and show the location update QPS: it makes clear that you need an in-memory geo index, not a DB.
  • Explain the geohash/H3 concept and neighbour-cell search.
  • Raise the double-assignment race condition yourself and solve it with locking/conditional updates.
  • Describe the order state machine out loud and how invalid transitions are blocked.
  • Mention city-wise sharding and isolation: geo systems naturally partition by city.