Skip to content
System Design

Learn

Design Uber

Designing a ride-hailing system: driver location ingestion, geospatial indexing with S2/H3 cells, the matching algorithm, trip state machine, surge pricing and payments.

4 min readUpdated 2 Sept 2026

#Geospatial#Matching#Real-time#State Machine

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/second

The 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.

youwhy hexagonsAll six neighbours are the samedistance from the centre, sowidening the search is a uniformring — squares mix edge andcorner neighbours.
Proximity search reads the target cell and its neighbours — points either side of a boundary can be metres apart with different cell ids.

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 driver

Ranking 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
     │          │
     └──────────┴──→ CANCELLED

Model 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 → dunning

Use 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.

Related