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)
| Item | Number | How it is derived |
|---|---|---|
| Online drivers/partners (peak) | 50 lakh | Assumption: India + global combined scale. |
| Location update QPS | ~12 lakh/sec | 50 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 ingress | 12.5 lakh x ~100 bytes (driver_id, lat, lng, ts, heading) = 125 MB/sec. |
| Orders/rides per day | 2 crore | Avg 230/sec, peak (dinner) 5x = ~1,200/sec; 10-20 geo queries per order. |
| Geo index memory | ~1 GB | 50 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 trace | 2 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/day | 2 crore orders x ~1 KB = 20 GB/day, ~7 TB/year; sharded SQL is enough. |
C.3 API design
| Endpoint | Purpose |
|---|---|
| POST /v1/drivers/location | Batch update of the driver location (also possible over a WebSocket/gRPC stream) |
| POST /v1/rides/estimate | Fare and ETA estimate for pickup/drop, with the surge multiplier |
| POST /v1/rides | Create a ride request or order; with an Idempotency-Key header so no double booking happens |
| POST /v1/rides/{id}/accept | The driver accepts the offer; with a conditional update only one driver wins |
| PATCH /v1/rides/{id}/status | State transition: arrived, picked_up, completed, cancelled |
| WS /v1/rides/{id}/track | Push the driver's live location to the rider (WebSocket) |
C.4 Data model
| Table / Store | Fields | Why |
|---|---|---|
| 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_at | The state machine and payment need ACID and conditional updates. |
| trip_events (Kafka -> Cassandra) | trip_id, event_type, ts, payload | An audit log of every state change; event sourcing and analytics. |
| location_history (Cassandra, partition key trip_id) | trip_id, ts, lat, lng | A write-heavy GPS trace; for fare disputes and route replay. |
| surge_multipliers (Redis) | key surge:{city}:{h3_cell} -> multiplier, expires_at | Read on every estimate, recomputed every 1-2 min. |
| drivers (SQL) | driver_id, vehicle_type, rating, city_id, kyc_status | Profile data rarely changes; relational. |
C.5 High-level architecture
- 1.Clients: Driver / Partner App (GPS every 4 s), Rider / Customer App (book, track), Restaurant App (accept, prep time)
- 2.Load Balancer -> WebSocket Gateway (persistent conn, live track, offers push) and API Gateway (auth, rate limit, Idempotency-Key)
- 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.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.Kafka: location + trip events, partitioned by city / trip_id
- 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.
| Component | What it does |
|---|---|
| Location Service | Takes driver location updates over WebSocket/gRPC, updates the Redis geo index (H3/geohash cell) and streams to Kafka. |
| Matching / Dispatch Service | Picks 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 Service | Runs the order state machine; every transition is a conditional update (version check) so invalid or duplicate transitions cannot happen. |
| ETA Service | Travel time from the road graph + live traffic + historical data; in food delivery the restaurant prep time is added too. |
| Pricing & Surge | Base fare + distance + time; a Flink stream job computes the surge multiplier from every cell's demand/supply ratio. |
| WebSocket Gateway | Persistent connections for riders and drivers; pushes live tracking and ride offers. |
| Notification & Payment Workers | Send push notifications, SMS and do payment capture idempotently from Kafka events. |
C.6 Main flows
Driver location update:
- The driver app sends the GPS location every 4 sec over a WebSocket/gRPC stream.
- The Location Service converts lat/lng into an H3 cell id.
- If the cell changed, remove the driver from the old cell set and add them to the new one (Redis).
- Latest location and status in the driver:{id} hash, TTL refreshed to 30 sec.
- The event goes to Kafka: for the surge calculator, active trip tracking and history.
- If the driver has an active trip, the WebSocket Gateway pushes the new location to the rider.
Ride request and driver matching:
- After seeing the estimate, the rider sends POST /v1/rides with an Idempotency-Key.
- The Trip Service creates the trip row with status = requested.
- Dispatch gets the available drivers from the pickup H3 cell and the surrounding rings (k-ring).
- It gets the actual road ETA of the top candidates from the ETA Service and ranks them (not straight-line distance).
- The offer is pushed to the best driver with a 10-15 sec timeout; on reject/timeout, the next driver.
- The driver accepts: UPDATE trips SET driver_id=?, status='assigned' WHERE id=? AND status='requested'.
- At the same time the driver's status in Redis becomes busy (with a lock) so no other offer reaches them.
- The rider gets the driver details and live ETA over the WebSocket.
Food order state machine:
- The customer places the order and payment is authorized; status = placed.
- The order goes to the restaurant app; on accept, status = accepted, with a prep time estimate.
- Dispatch assigns the nearest partner slightly before the prep time ends so the food does not get cold.
- The partner reaches the restaurant -> arrived, takes the food -> picked_up.
- Live tracking reaches the customer over a WebSocket; the ETA is recomputed on every update.
- On delivery status = delivered, with payment capture and partner payout through a Kafka worker.
- Every transition is validated: for example you cannot go back from delivered to picked_up.
C.7 Deep dives
- 1.Driver App: GPS, WS/gRPC batch
- 2.Location Service: lat/lng -> H3 cell id
- 3.Redis Geo Index: cell changed? SREM old, SADD new; driver:{id} HSET, TTL 30 s
- 4.Kafka location events -> Surge Calculator (Flink), History -> Cassandra / S3, Active trip -> WS push to rider
- 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.
- 1.Check only drivers in the pickup cell + the k=1 ring
- 2.Candidates D1, D2, D3: from Redis cell sets, status = free
- 3.ETA Service ranking: road ETA D2 4 min, D1 6 min
- 4.Offer to D2 for 10-15 s; reject / timeout -> next D1
- 5.Redis: SET lock:D2 NX TTL; only one wins (Ride A dispatch, D2 accepts; Ride B dispatch also offers to D2)
- 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
- 1.placed -> payment authorize
- 2.accepted: restaurant, prep time
- 3.assigned: partner, before prep finishes; cancelled possible
- 4.arrived: at the restaurant
- 5.picked_up: live tracking over WS
- 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
| Decision | What was chosen and why |
|---|---|
| Geohash vs H3 vs QuadTree | Chose 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 location | Redis: volatile data, lakhs of writes/sec, only the latest value is needed; durability comes from Kafka + Cassandra history. |
| Polling vs WebSocket for tracking | WebSocket: lower latency and overhead; but sticky connections and gateway scaling must be handled. |
| Greedy nearest vs batch matching | Greedy 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 consistency | Trip 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.