System Design Case Study

How does Uber match riders to the nearest available driver within 3 seconds?

?? Design a geo-spatial matching system: millions of drivers, <3s match, H3 indexing
Concepts Involved

Problem Statement

How does a ride-hailing platform match riders to the nearest available driver within 3 seconds, scoring candidates by distance, ETA, and driver rating while avoiding global scans of all drivers?

Core challenge: 5M+ drivers sending GPS updates every 4 seconds. A rider requests a ride. You must find the best driver within 2km in under 3 seconds without scanning all 5M drivers globally. The answer: spatial indexing with H3 hexagonal grid.
5M+
active drivers
GPS every 4 seconds
<3s
match latency
request ? driver assigned
~1.25M
GPS updates / sec
5M drivers · 4s interval
H3 Hex
spatial index
resolution 7 (~5km· cells)

Functional Requirements

Must Have

1. Ingest real-time GPS from all active drivers (every 4s)
2. On ride request, find nearest available drivers within radius
3. Score candidates by distance, ETA, rating, acceptance rate
4. Offer ride to best driver with 15s timeout
5. If declined/timeout, offer to next best (cascade)
6. Handle concurrent requests · don't offer same driver to two riders

Out of Scope

? Route calculation and navigation
? Surge pricing computation
? Payment processing
? Driver onboarding and verification
? Ride tracking after match

Non-Functional Requirements

PropertyTargetDesign Impact
Match Latency<3s request ? driver assignedRedis GEORADIUS (<2ms) + scoring (<5ms) + network to driver (<50ms)
GPS Ingestion1.25M updates/secRedis GEOADD sharded by city, TTL=10s auto-expire
Availability99.99% · rides can't fail to matchRedis Sentinel per city, fallback to expand radius
ConsistencyEventual · driver location 0-4s staleAcceptable: GPS updates every 4s, matching uses latest available
FairnessNo driver starved, no rider waiting foreverScoring balances distance + rating + acceptance rate + time since last ride
ConcurrencyNo double-offer (same driver to 2 riders)Redis SETNX lock per driver_id with 15s TTL

High-Level Architecture

H3 spatial index + Redis for real-time driver location + scoring service

Uber Driver Matching · End-to-End Architecture GPS INGESTION LAYER 5M Drivers GPS every 4s 1.25M updates/sec Location Service lat,lng ? compute H3 cell resolution 7 (~5.16 km·) Redis GEOADD per City GEOADD drivers:{city} lng lat driver_id TTL=10s auto-expire offline Kafka buffer ? Location Service ? Redis | Sharded by city (600+ cities) | SET driver:{id} ? {status, rating, vehicle} NYC: ~80K active drivers | GEORADIUS on 80K = ~2ms | ~500MB per city Redis node H3 Hexagonal Grid ?? ?? ?? ?? Uniform distance center?edge k-ring(1) = 7 cells · 15km O(1) lat,lng ? H3 index MATCHING LAYER · Find, Score, Lock Rider Request {pickup_lat, pickup_lng} ride_type, time Compute H3 lat,lng ? cell (1ms) + k-ring neighbors GEORADIUS 2km find nearby drivers ~10-20 candidates (2ms) Filter & Score available + correct vehicle ETA·0.4 + rating·0.3 + dist·0.3 SETNX Lock lock best driver (15s TTL) prevents double-offer Total scoring latency: H3 (1ms) + Redis GEORADIUS (2ms) + filter (1ms) + score (5ms) + lock (1ms) = ~10ms Redis Cluster per City Sentinel HA | 600+ independent instances DISPATCH LAYER · Offer, Timeout, Cascade Push Offer to Driver via push notification / WS show pickup, fare estimate network latency: ~50ms 15s Timeout driver must accept/decline if no response ? auto-decline DEL match_lock:driver_id Accept ? Match! ? Ride confirmed Decline ? cascade to next re-score remaining candidates Fallback: Expand 2km ? 5km ? 10km if all decline or none found show "searching..." to rider Match Flow Timeline · Latency Breakdown H3: 1ms Redis: 2ms Filter: 1ms Score: 5ms Network: 50ms Driver Decision: 0-15s Total: request ? first offer <60ms | request ? confirmed match <3s (avg driver accepts in ~2s) Concurrency: Redis SETNX lock prevents same driver offered to two riders | Cascade: up to 3 drivers before expanding radius Edge cases: GPS drift ? Kalman filter | City boundary ? k-ring query | No drivers ? expand + notify rider
Why H3 over Geohash? Geohash cells are rectangles · distance from center to edge varies. H3 hexagons have uniform distance from center to all edges. Neighbors are always equidistant. No "edge effects" where nearby points fall in distant cells. Uber open-sourced H3 specifically for this use case.
Scaling: Redis sharded by city. Each city has its own geospatial index. NYC alone: ~80K active drivers. GEORADIUS on 80K entries = ~2ms. Global: 600+ cities, each independently scaled. Driver location TTL=10s auto-removes offline drivers.
Edge cases: No drivers nearby ? expand radius (2km ? 5km ? 10km). All drivers decline ? re-score with lower threshold. GPS drift ? Kalman filter smoothing on driver app. City boundary ? query adjacent H3 cells (k-ring).

Scale Estimation

Back-of-envelope math for geo-spatial matching

Given: 5M active drivers · GPS every 4s · <3s match · 15M rides/day
StepDerivationResultDesign Impact
1GPS ingestion: 5M · 4s1.25M updates/secRedis GEOADD must handle 1.25M writes/sec (sharded by city)
2Match requests: 15M rides · 86400s~174 matches/sec avgLow QPS · matching logic can be complex per request
3Peak matches: 174 · 5· rush hour~870 matches/sec peakStill manageable · bottleneck is GPS ingestion, not matching
4Drivers per city (NYC): ~80K activeGEORADIUS on 80K = ~2msRedis handles single-city queries trivially
5Redis memory: 5M drivers · 100 bytes~500 MB totalFits in single Redis node per city (shard for safety)
6H3 cells (res 7): ~5.16 km· each~50K cells per cityEfficient spatial partitioning without scanning all drivers

Data Model

Redis geospatial for real-time location · PostgreSQL for ride history

H3 Hexagonal Grid · Rider requests from center cell, query k-ring(1) = 7 cells ?? Rider ??·3 ??·2 ??·1 empty ??·4 outside Match Flow (total <3s) ? lat,lng ? H3 cell index (1ms) ? GEORADIUS 2km on center + k-ring cells ? 10 candidates (2ms) ? Filter available + correct vehicle type (1ms) ? Score: ETA·0.4 + rating·0.3 + distance·0.3 ? rank (5ms) ? SETNX lock best driver ? push offer ? 15s timeout (50ms network)
// --- Driver Location (Redis Geospatial) ---
GEOADD drivers:nyc {lng} {lat} driver_123     // update position
GEORADIUS drivers:nyc {lng} {lat} 2 km COUNT 20 ASC  // find nearest 20

// --- Driver State (Redis Hash) ---
HSET driver:123 status "available" vehicle "sedan" rating 4.8 h3_cell "abc"
TTL: 10s (auto-expire if no GPS update ? driver considered offline)

// --- Ride Requests (PostgreSQL) ---
rides {
  ride_id:     UUID
  rider_id:    UUID
  driver_id:   UUID NULL          -- assigned after match
  pickup:      POINT(lat, lng)
  dropoff:     POINT(lat, lng)
  status:      ENUM(requested, matched, en_route, in_progress, completed, cancelled)
  city_id:     VARCHAR
  created_at:  TIMESTAMP
  matched_at:  TIMESTAMP NULL
}

// --- Match Lock (Redis) ---
SETNX match_lock:driver_123 ride_456 EX 15   // lock driver for 15s
// If driver declines or times out ? DEL match_lock:driver_123 ? offer to next

Resilience & Edge Cases

FailureImpactRecovery
Redis node failureDriver locations lost for one cityDrivers re-register on next GPS update (4s). Sentinel promotes replica.
No drivers in radiusRider waitsExpand radius: 2km ? 5km ? 10km. Show "searching..." with ETA.
Driver accepts but GPS staleDriver farther than expectedRe-verify location on accept. If too far, re-match.
Concurrent match (race)Two riders offered same driverRedis SETNX lock · only first succeeds. Second gets next candidate.
GPS spoofingFake driver locationVelocity checks (can't teleport), device attestation, trip verification.

Tech Stack & Tradeoffs

ComponentTechnologyWhyRejected
Spatial IndexRedis GEOADD + H3Sub-ms GEORADIUS, TTL auto-cleanup, sharded by cityPostGIS (too slow for 1.25M writes/sec), Elasticsearch (overkill)
Hex GridUber H3 (open-source)Uniform distance, no edge effects, O(1) lat/lng?cellGeohash (rectangular, edge effects), S2 (more complex)
LockRedis SETNX + TTLAtomic, distributed, auto-expires on timeoutDB lock (too slow), ZooKeeper (overkill for 15s lock)
GPS IngestionKafka ? Location Service ? RedisBuffer bursts, replay on failure, decouple from RedisDirect write to Redis (no buffer, lose data on Redis failure)
ScoringCustom scoring service (Go)Low-latency, ETA from routing service, configurable weightsML model (too slow for <5ms budget at this stage)

Interview Cheat Sheet

The 8 things to say for geo-matching design

1. H3 hexagonal grid · uniform distance, no edge effects, O(1) lat/lng ? cell
2. Redis GEOADD/GEORADIUS · sub-ms spatial queries on 80K drivers per city
3. TTL=10s on driver location · auto-removes offline drivers without explicit deregister
4. SETNX lock · prevents same driver offered to two riders simultaneously
5. Scoring: ETA · 0.4 + rating · 0.3 + distance · 0.3 · not just nearest, but best
6. Cascade on decline · 15s timeout, then offer to next-best candidate
7. City-level sharding · each city is independent Redis instance, no cross-city queries
8. Expand radius on failure · 2km ? 5km ? 10km, with rider notification of wait