Design Uber / Ride-Hailing
hardA rider opens the app and wants a car. Somewhere nearby, drivers are moving around. The system has to match them in a few seconds.
Key Concepts
High-level design
Driver: app → gRPC → location service → geo-sharded Redis (cell → driver list) + presence service.
Rider request: app → API gateway → dispatcher → geo index lookup → ETA scoring → offer service → driver app push.
Trip lifecycle: state machine in trip service (strongly consistent store), persisted on every transition.
Pricing: stream processor on demand/supply events → per-cell multipliers → quote service.
Payments: separate service; trip end triggers idempotent charge.
Analytics + Fraud: async pipelines from event stream.
Components
- Mobile gateway (long-polling or WebSocket for driver, stateless API for rider).
- Location service + geo-sharded Redis (per-cell driver lists).
- Dispatcher service (in-memory state per active request, persists snapshots).
- Pricing/surge stream processor (Flink/Spark Streaming).
- Trip service (Spanner / Vitess / sharded MySQL with sync replication).
- Payments service (separate, idempotent).
- Maps / ETA service (third-party or in-house OSRM/Valhalla).
- Notification service (push to driver app).
Capacity, worked
Drivers online at peak 5,000,000 GPS ping interval 4 seconds Ride requests at peak 5,000 / second --------------------------------------------------------------- Location writes 5M / 4 = 1,250,000 / second Write : request ratio 250 : 1
Location record in memory
driver id 8 B + lat/lon 16 B + heading, speed, ts 16 B
+ cell id 8 B = ~48 bytes
5M drivers x 48 B = 240 MB Historical trail (async, time-series)
1.25M writes/s x 48 B = 60 MB/second
= 5.2 TB/day
At 10:1 compression = 520 GB/day Candidate narrowing per request
Drivers in city 50,000
Target per cell 50-100
Cells read (centre + ring) 7
Candidates examined 350-700
Distance/ETA calls after filtering 10-20Consequences:
- 240 MB of live location fits in memory on one node, replicated. The
database never sees a ping.
- 5.2 TB/day of trail is a cost decision: sample it, compress it, or
expire it. Say which.
- 7 cell reads per request is why the grid exists; 5M distance
calculations per request is why nothing else works.
Dispatch without double-booking
The race
Two riders, same second, same nearby driver. Both matchers read
"available". Both send an offer. Both riders are told a car is coming. Fix 1 -- compare-and-set on the driver record
UPDATE drivers SET state='offered', offer_id=:id
WHERE driver_id=:d AND state='available'
Rows affected = 0 means someone else won; re-query for the next
candidate. One round trip, no locks held across a network call. Fix 2 -- short lease
Matcher takes a 10-second lease on the driver before offering. Lease
expiry returns the driver automatically, so a crashed matcher cannot
strand supply. The state machine, with timeouts
offline -> available driver goes online
available -> offered dispatch claims (CAS or lease)
offered -> assigned driver accepts
offered -> available driver declines, or 15s timeout
assigned -> on_trip pickup confirmed
assigned -> available rider cancels
on_trip -> available drop-offEvery transition out of `offered` and `assigned` needs a timeout. Without them, unanswered offers leak supply out of the pool one driver at a time.
Failure modes in production
GPS drift in dense areas
Symptom: drivers appear to jump between cells; matches look wrong.
Cause: multipath reflection between tall buildings.
Fix: smooth the track (map-match to roads), and require two consistent
readings before moving a driver across a cell boundary.
Cell hotspot at an airport or stadium
Symptom: one cell holds thousands of drivers; lookups slow down.
Cause: fixed cell size meeting non-uniform supply.
Fix: subdivide dense cells (H3 supports resolution changes), or cap
candidates and sort by ETA within the cap. Stranded supply
Symptom: available driver count falls through the evening without
drivers going offline.
Cause: offers with no timeout leaving drivers in `offered` forever.
Fix: timeouts on every non-terminal state, plus a sweeper that audits
for drivers whose state has not changed in an hour. Trail write backpressure
Symptom: location ingestion lags; live positions go stale.
Cause: the historical write path sharing a queue with live updates.
Fix: separate them. Live location is a memory overwrite; the trail is
a lossy sampled stream that is allowed to drop under pressure.