Uber is a geospatial matching problem wrapped in a state machine. Two things make it hard: the sheer write volume of location updates, and the fact that a driver must never be matched to two riders at once.
Requirements
Functional
- Riders request a ride; nearby drivers are found and one is matched.
- Live tracking of the driver before and during the trip.
- Fare estimate, surge pricing, payment on completion.
Non-functional
- Matching within a few seconds.
- Location updates handled at very high write volume.
- A driver is matched to exactly one rider — this is the correctness requirement.
5M active drivers, location update every 4 seconds
→ 1.25M writes/second, all of them tiny and immediately stale
1M ride requests/hour ≈ 300/secondThe asymmetry is the design driver: **location writes outnumber ride requests by roughly
4,000 to 1.**
Location ingestion
Do not put 1.25M writes/second into a relational database.
Driver app → location service → Redis (current position per driver, with TTL)
→ Kafka → historical store (for analytics, replay, pricing)Two separate paths. Current location is **hot, tiny, overwritten constantly, and worthless once superseded** — that is a cache, not a database. History goes to a stream for anything that needs it later. Keeping these separate is the single most important decision here.
A TTL on the Redis entry doubles as availability detection: a driver whose updates stop disappears from matching automatically.
Geospatial indexing
Finding drivers within 3 km cannot be a scan over 5M rows. Divide the world into cells —
S2 (Uber originally) or H3 (Uber's own hexagonal system) — and index drivers by cell id.
Hexagons (H3) beat squares for this: every neighbour is equidistant from the centre, so "expand the search radius" is a uniform ring rather than a mix of edge and corner neighbours at different distances.
Cell size is a trade-off — too big and you scan too many drivers, too small and you must query many cells. Denser cities warrant finer resolution.
Matching
1. Rider requests → find candidate drivers in nearby cells
2. Filter: available, vehicle type, rating, not currently assigned
3. Rank: ETA to pickup (via the ROAD network, not straight-line distance)
4. Offer to the best driver, with a timeout (~15 s)
5. Accept → assign. Decline or timeout → next driverRanking by road-network ETA rather than straight-line distance matters: a driver 500 m away across a river may be 15 minutes out. That is a call into the routing service.
The correctness requirement lives in step 5. Two riders must not be offered the same
driver simultaneously. Options: a distributed lock on the driver id for the offer window, or
an atomic compare-and-set on the driver's state (available → offered) so only one request
wins. The compare-and-set is preferable — locks that need timeouts introduce their own
failure modes.
The trip state machine
REQUESTED → MATCHED → DRIVER_ARRIVING → IN_PROGRESS → COMPLETED
│ │
└──────────┴──→ CANCELLEDModel this explicitly. Every transition is an event, persisted, and drives notifications to both parties. State transitions must be idempotent — mobile networks retry, and "driver arrived" arriving twice must not charge a second waiting fee. Store the trip in a strongly consistent store; this is money and safety, not a feed.
Live tracking
During a trip, both apps hold a WebSocket. Driver positions stream to the rider through the same gateway-and-registry pattern as chat. Between updates the client interpolates along the route so the car appears to move smoothly rather than teleport every four seconds — a small detail that shows product thinking.
Surge pricing
Computed per cell over a short window: demand (open requests) versus supply (available drivers). A ratio above a threshold raises the multiplier, which suppresses demand and attracts drivers.
The subtlety worth mentioning: the price must be locked at request time. A fare that changes mid-trip is unacceptable, so the quote is a short-lived token the trip carries. Smoothing matters too — a multiplier that oscillates every 30 seconds is a bad experience, so apply hysteresis.
Payments
Payment happens after the trip and must not block trip completion.
COMPLETED → enqueue payment job → payment service → gateway
↓ failure
retry with backoff → dunningUse an idempotency key per trip so a retried charge does not bill twice — see payment gateway. A pre-authorisation at request time catches invalid cards before the ride rather than after.
Follow-ups to expect
- Driver assignment fairness — pure nearest-driver starves drivers in quiet areas.
- Batched matching — collecting requests for a few seconds and solving an assignment
problem globally beats greedy first-come matching on total wait time.
- Pool / shared rides — matching becomes a routing problem with detour constraints.
- Region isolation — shard by city; cross-city coordination is rarely needed and city
isolation limits blast radius.
- Offline drivers — TTL expiry removes them from matching automatically.
- Fraud — GPS spoofing detection, trips with implausible trajectories.
Track this problem on the System Design sheet.
Frequently asked
How does Uber find nearby drivers efficiently?
By dividing the world into cells — S2 or Uber's hexagonal H3 system — and indexing driver ids by cell in Redis. A search reads the rider's cell plus its neighbours, then filters by exact distance. Hexagons help because every neighbour is equidistant from the cell centre, so expanding the radius is a uniform ring rather than a mix of edge and corner neighbours.
How do you handle a million driver location updates per second?
Split the write path. Current position goes to an in-memory store like Redis with a TTL, because it is tiny, constantly overwritten and worthless once superseded — that is a cache, not a database. A copy streams to Kafka for history, analytics and pricing. The TTL doubles as availability detection: a driver whose updates stop drops out of matching automatically.
How do you prevent the same driver being matched to two riders?
With an atomic compare-and-set on the driver's state — only the request that successfully moves the driver from available to offered proceeds, and the other must pick a different driver. A distributed lock over the offer window also works but introduces timeout-related failure modes, so the atomic state transition is generally preferable.