Handbooks / System Design Case Studies / Chapter 13
Design a Ride-Hailing App like Uber
12 pages · ~18 min✓ Reviewed
Builds on Design a Food Delivery App like Zomato. Next up: Design a Ticket Booking System like BookMyShow.
Problem, Scope and Requirements (Ride-Hailing App)
1. Scope
- Request ride: rider picks pickup and dropoff
- Match nearby driver: find and offer to the best one
- Live tracking: rider sees the driver on a map
- Trip lifecycle: requested, assigned, in progress, completed
- Fare and payment: estimate up front, charge at the end
- Out: ride pooling (shared rides)
- Out: scheduled rides (book for later)
- Out: driver onboarding (documents, background checks)
2. Functional Requirements
- Requestwith fare estimate
- Offernearest driver
- Accept / declinedriver decides
- Tracklive trip tracking
- Pay and rateafter the trip
High-Level Architecture (Ride-Hailing App)
1. Edge and Core Services
- API gateway: REST for rides, fares and payments; handles auth and rate limits
- WebSocket gateway: live channels for drivers and riders
- ~1.5M persistent connections ÷ ~50K per node ≈ 30+ gateway nodes
- Each service owns its own datastore; no service reads another's tables
| Service | Job | Owns |
|---|
Location Updates and Tracking Service (Ride-Hailing App)
1. Responsibility and Design
- Ingest driver GPS pings every 4s (every 1s during a trip)
- Keep the latest position per driver
- During a trip, fan out each ping to the rider
- Load: 1.5M drivers ÷ 4s ≈ 375K writes/s, ~100 B each ≈ 37 MB/s
- Driver appGPS ping
- WebSocket gatewaypersistent conn
- Location servicevalidate
- Redislatest, TTL 30s
- Kafkaappend history
Geospatial Indexing: Geohash, Quadtree, H3 (Ride-Hailing App)
1. Geohash
- Geohash interleaves lat/lng bits into a base32 string
- Shared prefix = nearby cells
- Precision 6 ≈ 1.2 km × 0.6 km cell
- Precision 7 ≈ 150 m cell
- Stored as a Redis sorted set or a prefix index
- Neighbors across a cell edge can have totally different prefixes
- A prefix match alone misses drivers just over the edge
- Cell shapes distort near the poles
Driver Matching and Dispatch (Ride-Hailing App)
1. Job and Candidate Search
- Pick the best available driver for a ride request
- Match SLA: 2s p99 to produce an offer
- 5–15s end to end, including the driver accept
- Query the H3 res-8 cell plus
k-ring(1–2)forstatus=AVAILABLE - Drop candidates whose
driver:{id}is missing or whosetsis stale - Rank by ETA to pickup, not straight-line distance
Trip State Machine (Ride-Hailing App)
1. States
- 1REQUESTEDrider asks
- 2MATCHINGdispatch runs
- 3DRIVER_ASSIGNEDdriver accepted
- 4DRIVER_ARRIVEDat pickup
- 5IN_PROGRESStrip started
- 6COMPLETEDfare computed
- 7PAIDcharge captured
- CANCELLED: rider or driver cancels, from any state before IN_PROGRESS
- NO_DRIVER_FOUND: MATCHING exhausted its attempts and radius expansion
- Terminal states: PAID, CANCELLED, NO_DRIVER_FOUND
2. Guarded Transitions
ETA and Routing Service (Ride-Hailing App)
1. What the Service Does
- Target: p99 < 100 ms per estimate
- Load: ~100K estimates/s at peak
- Callers: Matching, Trip and Pricing services
| Estimate | Used for | Caller |
|---|---|---|
| Pickup ETA | ranking drivers | Matching |
| Trip ETA |
Surge Pricing (Ride-Hailing App)
1. What Surge Does
- Goal: balance supply and demand per H3 cell
- multiplier = f(open ride requests ÷ available drivers)
- Recomputed every 1–2 minutes, not per request
- Higher price pulls drivers in and trims marginal demand
2. Pipeline and Example
- Kafkatrip requests + driver availability
- Flink jobcount per H3 res-8 cell, sliding 5-min window
- Redissurge:{cell} = multiplier
Payments and Fare Settlement (Ride-Hailing App)
1. Responsibility and Flow
- Authorize on request, charge on trip end, pay out drivers, handle refunds and tips
- Load: ~230 charges/s average (20M trips/day), 700/s peak
- Hard rule: never charge twice, so payment is strongly consistent
- 1Pre-authorizehold estimate + 20% at request
- 2COMPLETEDfinal = base + per-km + per-min × surge
- 3Capturecharge the final fare on the held card
- 4Ledger entryone DB transaction
- 5Driver payoutweekly batch
2. Idempotency
Handling Driver and Rider Disconnects (Ride-Hailing App)
1. Detecting a Dead Connection
- Heartbeat: WebSocket ping every 5s in both directions
- 15s with no heartbeat → connection marked suspect
- 30s with no location →
driver:{id}expires (TTL 30s) - Suspect is a flag only: nothing is removed from the geo index yet
- 10slast heartbeat
- 25sheartbeat missed
- 315sconnection suspect
- 430sdriver:{id} TTL expires
- 5~30slistener ZREMs from GEO set
Scaling, Failure Handling, Trade-offs and Interview Questions (Ride-Hailing App)
1. Scaling and Failure Handling
| Layer | Sharded by | Why |
|---|---|---|
| Redis geo index | city or H3 parent cell | Hot cities get dedicated shards |
| Trips (SQL) | trip_id | Even spread, one trip = one shard |
| Kafka topics | driver_id or cell | Per-driver ordering kept |
Check yourself
Two drivers are 50 m apart but sit on opposite sides of a geohash cell border. A rider's pickup is in cell A, and the service queries only cell A. Driver 2 is in cell B and is closer than anyone else. What happens, and what is the fix?
- Driver 2 is missed, so the rider is matched to a farther driver or to nobody.
- Geohash prefixes only guarantee that a shared prefix means close. Close does not guarantee a shared prefix.
- Fix: query the pickup cell plus its 8 neighbors. With H3, query the k-ring (k=1 or 2).
- Then rank by ETA, not straight-line distance.