← Back to all questions
StaffGeospatialReal-Time

Design a Real-Time Ride Matching System

A Staff-level walkthrough of a real-time dispatch engine that matches riders to drivers in under two seconds across 10,000+ cities — built on an H3 hexagonal geospatial index, an expanding-ring candidate search, and multi-factor scoring that trades pickup ETA against driver fairness and supply health. It co-locates the driver index with matching compute by sharding on geographic cells, degrades gracefully when the ETA or location pipelines fail, and rebuilds lost in-memory state from drivers' phones after a data-center outage. Follow it top to bottom, or jump to any step.

Level
Staff
Category
Geospatial · Real-Time Systems
Interview time
60 min
100% free · No login required
WHAT THIS QUESTION TESTS
·H3 hexagonal indexing with expanding k-ring candidate search
·Three-phase pipeline: candidate generation → scoring → dispatch
·Multi-factor scoring across ETA, fairness, rating, and supply health
·15-second offer window with cascade and radius expansion on decline
★ STAFF-LEVEL SIGNALS
Consistent-hash sharding by geographic cell co-locates index and compute for zero-hop lookups
Fairness and anti-starvation are first-class scoring terms, defending driver retention
Driver-phone encrypted state digest recovers a failed region in ~30s, not minutes
Shadow → A/B → city-by-city rollout with per-city flags and cold-standby legacy
0

Scope & ambiguity

Framing statement:

“Before I dive in, let me frame how I’d approach this. Uber’s ride matching system is the core of a two-sided marketplace that spans multiple teams — a geospatial platform team, a dispatch optimization team, a marketplace/pricing team, a supply management team, and a real-time data infrastructure team. I’d like to focus on the most architecturally interesting challenge: the real-time dispatch engine — specifically, how we match riders with optimal drivers in under 2 seconds considering proximity, ETA, fairness, and supply-demand balance across 10K+ cities. I’ll propose a phased approach.”

Phased delivery:

Phase
Timeline
Scope
Phase 1
0-3 months
Core H3-based geospatial matching with ETA optimization, single-rider dispatch for top 50 cities, <2s match latency, basic fairness scoring
Phase 2
3-9 months
UberPool/shared ride matching, ML-powered scoring (cancellation prediction, ETA improvement via DeepETA), surge pricing integration, expansion to 500+ cities
Phase 3
9-18 months
Unified matching across Rides/Eats/Freight, predictive supply positioning, autonomous vehicle integration, multi-modal trip planning (transit + Uber combo)

Team ownership:

Team
Ownership
Dispatch Optimization (DISCO) Team
Matching algorithm, candidate scoring, offer management, dispatch orchestration
Geospatial Platform Team
H3 indexing, driver location tracking, spatial queries, map services
Marketplace/Pricing Team
Surge pricing, supply-demand balancing, fare estimation, incentive programs
Supply Management Team
Driver state machine, availability tracking, driver preferences, earnings management
Real-Time Data Infrastructure Team
Kafka pipelines, location ingestion, Ringpop coordination, real-time analytics
ML Platform (Michelangelo) Team
ETA prediction models, cancellation prediction, demand forecasting, model serving

Key ambiguity I’d challenge:

“When we say ’match in under 2 seconds,’ I want to clarify: is that from the moment the rider taps ’Request’ to when a driver receives the offer, or until the driver accepts? The architecture changes significantly — if we include driver acceptance (15-second window), we have cascading fallback logic. I’ll design for sub-2s to first driver offer, with a total match budget of ~30s including retries.”
1

Requirements

Functional Requirements (Phase 1)

  • FR1: Rider requests a ride and is matched with an optimal nearby driver in real-time
  • FR2: System tracks driver locations (GPS updates every 4 seconds) and maintains a live geospatial index
  • FR3: Matching considers ETA, driver rating, fairness (wait-time equity), and supply health
  • FR4: Driver receives ride offer with 15-second accept window; on decline, system cascades to next-best driver
  • FR5: System supports multiple ride types (UberX, XL, Black, Comfort) with vehicle-type filtering
  • FR6: Rider sees real-time ETA updates and driver position after match confirmation

Non-Functional Requirements

Requirement
Target
Match latency (request to first driver offer)
p50 < 500ms, p99 < 2s
ETA prediction accuracy
within 2 min of actual arrival 90% of the time
Availability
99.99% (revenue loss at ~$400K/min of downtime)
Location update throughput
~2M GPS updates/sec (8.8M drivers, ~4s interval, ~50% online)
Matching throughput
~420 requests/sec (36M trips/day) peak ~1200 req/sec (3x avg)
Driver index freshness
GPS position reflected in index within 1 second of receipt
Data durability (trip records)
99.999999999% (11 nines)
Geographic coverage
10,000+ cities across 70+ countries

Organizational Context

  • Existing system: This is a re-architecture of the DISCO (Dispatch Optimization) platform, not greenfield — must coexist with legacy dispatch during migration
  • Multi-product: Matching engine must eventually serve Rides, Eats, and Freight (unified Fulfillment Platform), so abstractions should not be rides-specific
  • Regulatory: Data residency requirements per country (EU GDPR, China data localization), driver labor laws vary by jurisdiction
  • Timeline: Phase 1 in 3 months with the DISCO team (12-15 engineers) plus support from Geospatial and Infra teams

Out of Scope (Explicitly)

  • Fare calculation and payment processing (Marketplace/Pricing team)
  • Driver onboarding and identity verification
  • Navigation/routing engine (separate Maps team)
  • Rider/driver chat and in-trip experience
  • Uber Eats-specific matching (restaurant prep time adds different constraints)
2

Back-of-envelope estimation

Scale Numbers

Metric
Value
Monthly Active Platform Consumers (MAPCs)
~200M
Active drivers and couriers
~8.8M
Trips per day
~36M (~420/sec avg, ~1200/sec peak)
Concurrent active trips (peak)
~5M
GPS location updates
~2M/sec (drivers pinging every 4s)
Cities served
10,000+ across 70+ countries
Peak concurrent online drivers
~4.4M

Storage Estimation

Data
Calculation
Result
Driver location index (in-memory)
4.4M drivers x 128 bytes (id, h3, lat, lng, heading, status, vehicle)
~564MB
GPS location stream (daily)
2M updates/sec x 86,400s x 100 bytes
~17TB/day
Trip records (annual)
36M trips/day x 365 x 2KB per trip
~26TB/year
Matching decision logs (30 days)
36M trips/day x 30 x 5KB (candidates, scores, decision)
~5.4TB
Total hot storage
~6TB
Total archival (annual)
~150TB+

Cost Estimation

Resource
Calculation
Monthly Cost
Compute (matching + scoring)
500 servers (Go, high-CPU) x $0.40/hr
~$146K
In-memory driver index (Redis clusters)
200 nodes x $0.50/hr
~$73K
Kafka (location + events)
100 brokers handling 2M msg/sec
~$220K
Cassandra (trip + decision logs)
300 nodes for ~6TB hot
~$110K
ML model serving (Michelangelo)
100 GPU instances for ETA/scoring
~$180K
Bandwidth (GPS ingestion)
~2M msg/sec x 100B x 86400 = ~17TB/day
~$400K
Total
~$1.1M/month

Architectural implication: GPS ingestion bandwidth and Kafka infrastructure dominate costs. We need efficient binary protocols for location updates (not JSON), aggressive batching on driver-side, and tiered storage (hot location data in Redis, warm in Cassandra, cold in HDFS). The matching compute itself is surprisingly cheap relative to data ingestion — the bottleneck is I/O, not CPU.

3

API design

External API (Client-Facing)

# Ride Request
POST /api/v1/rides/request -- Rider requests a ride
Body: { pickup: {lat, lng}, dropoff: {lat, lng}, ride_type: "UberX", payment_method_id }
Response: { ride_id, status: "MATCHING", estimated_pickup_eta }
 
GET /api/v1/rides/{ride_id} -- Get ride status and details
DELETE /api/v1/rides/{ride_id} -- Cancel ride request
 
# Driver Offer (Push via WebSocket/gRPC stream)
WSS /api/v1/driver/stream
-- Server sends: { type: "ride_offer", ride_id, pickup, dropoff, fare_estimate, accept_deadline }
-- Driver sends: { type: "offer_response", ride_id, accepted: true/false }
 
# Location Updates (Driver)
POST /api/v1/drivers/location -- Batch GPS update (binary protobuf)
Body: { driver_id, locations: [{lat, lng, heading, speed, timestamp}] }
 
# Supply Query (Internal, used by rider app for ETA display)
GET /api/v1/supply/eta?lat={lat}&lng={lng}&ride_type={type}
Response: { eta_seconds, surge_multiplier, available_types: [...] }
 
# Ride Types Available
GET /api/v1/rides/estimate?pickup_lat&pickup_lng&dropoff_lat&dropoff_lng
Response: [{ ride_type, fare_estimate, eta_seconds, surge_multiplier }]

Inter-Service Contracts

SERVICE: DISCO (Dispatch Optimization)
OWNER: Dispatch Optimization Team
PROTOCOL: gRPC (internal), REST (client-facing via API Gateway)
 
RPC MatchRide(MatchRideRequest) returns (MatchRideResponse)
- Input: rider_id, pickup_h3, dropoff_h3, ride_type, constraints
- Output: matched_driver_id, eta_seconds, score, candidates_evaluated
- Latency SLA: p99 < 500ms
- Availability SLA: 99.99%
- Auth: mTLS between services
 
RPC GetCandidateDrivers(CandidateRequest) returns (CandidateResponse)
- Input: h3_cell, radius_rings, ride_type, max_candidates
- Output: [{ driver_id, lat, lng, heading, eta_to_pickup, status }]
- Latency SLA: p99 < 100ms
 
EVENTS PUBLISHED:
Topic: dispatch.ride-offers
Schema: RideOffer (Protobuf)
Events: OFFER_SENT, OFFER_ACCEPTED, OFFER_DECLINED, OFFER_EXPIRED
Partition key: driver_id
Retention: 7 days
 
Topic: dispatch.match-decisions
Schema: MatchDecision (Protobuf)
Events: MATCH_STARTED, CANDIDATES_SCORED, MATCH_COMPLETED, MATCH_FAILED
Partition key: ride_id
Retention: 30 days
SERVICE: SupplyService (Driver Location & State)
OWNER: Supply Management Team
PROTOCOL: gRPC
 
RPC UpdateDriverLocation(LocationUpdate) returns (Ack)
- Latency SLA: p99 < 50ms
- Throughput: 2M updates/sec
 
RPC GetDriverState(DriverStateRequest) returns (DriverState)
- Returns: {status, current_trip, vehicle_type, preferences, heading}
- Latency SLA: p99 < 20ms
- Consistency: Read-your-writes
 
EVENTS PUBLISHED:
Topic: supply.location-updates
Schema: DriverLocation (Protobuf, compact binary)
Events: LOCATION_UPDATED, DRIVER_ONLINE, DRIVER_OFFLINE
Partition key: h3_resolution4_cell (geographic sharding)
Retention: 24 hours (hot), 30 days (warm)
SERVICE: ETAService
OWNER: ML Platform (Michelangelo) Team
PROTOCOL: gRPC
 
RPC PredictETA(ETARequest) returns (ETAResponse)
- Input: origin_lat, origin_lng, dest_lat, dest_lng, departure_time
- Output: eta_seconds, confidence_interval
- Latency SLA: p99 < 50ms (served from Michelangelo model cache)
- Model: DeepETA (deep learning, retrained hourly)

API Governance

  • Versioning: URI versioning for external (/v1/), semantic versioning for internal Protobuf schemas
  • Schema registry: Protobuf for all inter-service and Kafka event schemas, enforced via CI/CD schema compatibility checks
  • Rate limiting: Per-rider: 5 ride requests/min, Per-driver: 60 location updates/min (batched), Per-city: adaptive based on supply capacity
  • Idempotency: All ride requests carry client-generated idempotency key to prevent duplicate dispatches on network retry
4

Data model

Core Entities

Entity: Trip
- trip_id: UUID (PK)
- rider_id: UUID (FK to Rider)
- driver_id: UUID (FK to Driver, nullable until matched)
- status: ENUM (REQUESTING, MATCHING, DRIVER_ASSIGNED, EN_ROUTE_PICKUP,
ARRIVED_PICKUP, IN_TRIP, COMPLETED, CANCELLED)
- ride_type: ENUM (UBERX, UBERXL, BLACK, COMFORT, POOL, GREEN)
- pickup_location: POINT (lat, lng)
- pickup_h3_res9: BIGINT (H3 index at resolution 9)
- dropoff_location: POINT (lat, lng)
- dropoff_h3_res9: BIGINT
- fare_estimate_cents: INT
- surge_multiplier: DECIMAL(3,2)
- request_time: TIMESTAMP
- match_time: TIMESTAMP
- pickup_time: TIMESTAMP
- dropoff_time: TIMESTAMP
- city_id: INT
 
Entity: DriverSupply
- driver_id: UUID (PK)
- status: ENUM (OFFLINE, ONLINE_AVAILABLE, ON_TRIP, ON_TRIP_ACCEPTING)
- current_location: POINT (lat, lng)
- current_h3_res9: BIGINT (H3 cell, updated every 4s)
- heading_degrees: SMALLINT
- speed_mps: FLOAT
- vehicle_type: ENUM (SEDAN, SUV, LUXURY, GREEN)
- eligible_ride_types: SET<ENUM> (derived from vehicle)
- current_trip_id: UUID (nullable)
- destination_mode: POINT (nullable, driver's preferred heading direction)
- last_location_update: TIMESTAMP
- consecutive_idle_seconds: INT
- daily_trips_completed: INT
- daily_earnings_cents: INT
- acceptance_rate_30d: DECIMAL(3,2)
 
Entity: MatchDecision
- match_id: UUID (PK)
- trip_id: UUID (FK)
- candidate_drivers: LIST<CandidateScore> (serialized)
- selected_driver_id: UUID
- algorithm_version: VARCHAR
- match_latency_ms: INT
- search_radius_km: FLOAT
- h3_cells_searched: INT
- candidates_evaluated: INT
- created_at: TIMESTAMP
 
Nested: CandidateScore
- driver_id: UUID
- eta_seconds: INT
- distance_km: FLOAT
- score: FLOAT
- score_components: MAP<STRING, FLOAT> (eta, fairness, supply_health, rating)
- reason_excluded: VARCHAR (nullable)
 
Entity: RideOffer
- offer_id: UUID (PK)
- trip_id: UUID (FK)
- driver_id: UUID (FK)
- offer_sequence: INT (1st, 2nd, 3rd attempt)
- status: ENUM (PENDING, ACCEPTED, DECLINED, EXPIRED)
- offered_at: TIMESTAMP
- responded_at: TIMESTAMP (nullable)
- expiry_seconds: INT (default 15)

Data Ownership Boundaries

Data
Owner Service
Access Pattern for Others
Trip lifecycle & state
TripService (rt-demand)
Sync gRPC, events on dispatch.trips topic
Driver location & status
SupplyService (rt-supply)
In-memory index via gRPC, events on supply.location-updates
Match decisions & scoring
DISCO
Event stream (dispatch.match-decisions), async read via gRPC
ETA predictions
ETAService (Michelangelo)
Sync gRPC (p99 < 50ms, cached per H3 cell pair)
Surge pricing & multipliers
PricingService
Sync gRPC (cached 30s per H3 res7 cell)
Driver preferences & profiles
DriverProfileService
Sync gRPC (heavily cached, invalidated on change)

Storage Technology Choices

Data
Technology
Rationale
Alternative Considered
Driver live index
Redis Cluster (in-memory)
Sub-ms geospatial lookups, ~564MB fits in memory, supports sorted sets for H3 cell membership
Memcached (rejected: no sorted sets or pub/sub)
Trip records
Cassandra (Schemaless)
High write throughput, tunable consistency, Uber's Schemaless layer on top for schema evolution
PostgreSQL (rejected: single-node write bottleneck at 420 writes/sec peak)
Match decision logs
Cassandra
Append-heavy write pattern, TTL for automatic expiration
Bigtable (rejected: not in Uber's core stack)
Location event stream
Kafka
2M msg/sec throughput, durable replay, geographic partitioning by H3 cell
Pulsar (rejected: Kafka deeply integrated in Uber's infra)
ML feature store
Cassandra + Redis
Michelangelo uses Cassandra for offline features, Redis for real-time features
Custom store (rejected: Michelangelo already provides this)
Analytics & historical
HDFS / Hive
Batch analytics on trip data, demand forecasting training
S3 (rejected: Uber runs on-prem Hadoop clusters)

Data Lifecycle

HOT (0-24 hours): Driver locations in Redis, active trip state in memory + Cassandra,
match decisions in Cassandra with full candidate lists
WARM (1-30 days): Trip records in Cassandra, match decisions (summary only, candidate
lists pruned), location data in Kafka (7-day retention) then Cassandra
COLD (30+ days): Trip records archived to HDFS/Hive, match logs aggregated for analytics,
raw GPS traces in compressed Parquet on HDFS
5

High-level architecture

Architecture Diagram

[Rider App] [Driver App]
| |
[REST/WS] [gRPC stream + GPS]
| |
[API Gateway / Envoy] [API Gateway / Envoy]
| |
+-------------+-------------+ +-----------+-----------+
| | | |
[Trip Service] [ETA Display] [Supply Service] [Offer Handler]
(rt-demand) (read path) (rt-supply) (push to driver)
| |
| +--- Ride Request Event ----+ |
| | | |
v v v v
[DISCO — Dispatch Optimization Service]
(Ringpop-sharded by H3 res4 cell)
| | | |
v v v v
[Candidate [Scoring [Offer [Fallback
Generator] Engine] Manager] Logic]
| |
v v
[Driver Index] [ETAService] [PricingService] [FairnessService]
(Redis H3) (Michelangelo) (Surge/Pricing) (Wait-time equity)
^
| (GPS every 4s)
|
[Location Ingestion Pipeline]
^
|
[Kafka: supply.location-updates]
^
|
[Driver App GPS → API Gateway → Location Writer]
 
--- Async Pipelines ---
 
[Kafka: dispatch.match-decisions] → [Analytics Pipeline] → [HDFS/Hive]
[Kafka: dispatch.ride-offers] → [Driver Metrics] → [Cassandra]
[Kafka: supply.location-updates] → [Demand Forecasting] → [Michelangelo]
→ [Heat Map Generator] → [Redis (driver app)]

Service Boundaries with Ownership

Service
Team
Data Store
Scaling
Consistency
SLA
API Gateway
Platform Infra
Horizontal, per-region
p99 < 10ms overhead
Trip Service (rt-demand)
Dispatch Optimization
Cassandra (Schemaless)
Horizontal, sharded by city_id
Eventual (Cassandra quorum reads)
p99 < 100ms
Supply Service (rt-supply)
Supply Management
Redis + Cassandra
Horizontal, sharded by H3 res4 cell via Ringpop
Eventual (AP, availability over correctness)
p99 < 50ms
DISCO (Dispatch)
Dispatch Optimization
In-memory (stateful, Ringpop)
Sharded by H3 res4 geographic cells
Best-effort (match quality vs speed tradeoff)
p99 < 500ms per match
ETA Service
ML Platform
Michelangelo model cache
Horizontal, GPU instances
— (stateless prediction)
p99 < 50ms
Pricing Service
Marketplace
Redis (surge cache) + Cassandra
Horizontal, per-city
Eventual (surge prices cached 30s)
p99 < 30ms
Offer Manager
Dispatch Optimization
Redis (offer state)
Horizontal
Strong (offer state must not be lost)
p99 < 20ms

Sync vs Async Communication

Communication
Type
Rationale
Rider request → Trip Service → DISCO
Sync (gRPC)
Rider waiting for match, latency-critical
DISCO → Driver Index (candidate lookup)
Sync (gRPC / in-process)
Part of match critical path
DISCO → ETA Service (per-candidate ETA)
Sync (gRPC, batched)
Scoring requires ETA, critical path
DISCO → Offer Manager → Driver App
Sync (gRPC → push notification / stream)
Driver must receive offer immediately
Driver GPS → Location Ingestion → Redis
Async (Kafka → consumer → Redis write)
High-throughput, can tolerate ~1s lag
Match decisions → Analytics
Async (Kafka event)
Analytics can lag, not user-facing
Supply changes → Demand Forecasting
Async (Kafka → Michelangelo training)
ML retraining is batch/periodic

Failure Domain Boundaries

FAILURE DOMAIN 1: Matching critical path
[Trip Service] → [DISCO] → [Driver Index] → [ETA Service]
Impact: New ride requests cannot be matched. Active trips unaffected.
Mitigation: Ringpop re-shards around failed DISCO nodes in <10s.
Degraded mode: Fall back to nearest-driver-only matching (skip ML scoring).
 
FAILURE DOMAIN 2: Driver location pipeline
[Driver App GPS] → [Kafka] → [Location Consumer] → [Redis Index]
Impact: Driver positions become stale. Matching still works with stale data
(positions age gracefully — a 30s-old position is still useful).
Mitigation: Driver app caches last-known position; DISCO uses stale-but-valid index.
 
FAILURE DOMAIN 3: Offer delivery
[Offer Manager] → [Push Gateway] → [Driver App]
Impact: Driver never receives offer. Offer expires after 15s, DISCO retries with next driver.
Mitigation: Redundant push channels (FCM + WebSocket), offer timeout with automatic cascade.
 
FAILURE DOMAIN 4: ML/Pricing services
[ETA Service] → [Michelangelo] | [Pricing Service]
Impact: Cannot compute accurate ETAs or surge prices.
Mitigation: ETA falls back to distance-based heuristic (distance / avg_speed).
Pricing falls back to last-known surge multiplier (cached in Redis, TTL 5 min).
6

Deep dives

WHERE STAFF IS WON

Deep Dive 1: Geospatial Indexing with H3

Core Decision: Spatial Index Selection

Dimension
Geohash
S2 Cells
H3 (Hexagonal)
Cell shape
Rectangle (varies by latitude)
Quadrilateral (mostly square)
Hexagon (uniform)
Neighbor distance
Non-uniform (corner vs edge)
Non-uniform
All neighbors equidistant
Query pattern
Prefix match
Cell containment
Hex ring traversal
Resolution levels
Variable (1-12 chars)
30 levels
16 levels
Edge artifacts
Significant at cell boundaries
Moderate
Minimal (hex tiling)
Used by
Elasticsearch, DynamoDB
Google Maps
Uber (developed it)

Decision: H3 (Uber’s own library)

H3 is the clear choice because Uber developed it specifically for ride matching. The hexagonal grid means all six neighbors are equidistant from the center — unlike square grids where diagonal neighbors are ~1.41x farther. This property is critical for expanding-ring searches: when we search “1 ring out,” every neighboring cell represents the same additional distance.

Resolution strategy:

Resolution 4: ~1,770 km² — used for DISCO Ringpop sharding (1 shard = metro area)
Resolution 7: ~5.16 km² — used for surge pricing zones and demand forecasting
Resolution 9: ~0.105 km² — used for driver indexing and candidate search (~100m cells)
Resolution 12: ~0.003 km² — used for precise pickup point matching

Driver index structure (in Redis):

Per H3 resolution 4 shard (owned by one DISCO instance via Ringpop):
 
Sorted Set per H3 res9 cell:
Key: "drivers:{h3_res9_cell_id}"
Members: driver_id
Score: last_update_timestamp (for staleness detection)
 
Hash per driver:
Key: "driver:{driver_id}"
Fields: h3_res9, lat, lng, heading, speed, status, vehicle_type,
eligible_ride_types, current_trip_id, idle_since
 
Update flow (every 4 seconds per driver):
1. Remove driver from old H3 cell sorted set
2. Add driver to new H3 cell sorted set
3. Update driver hash with new position
Atomicity: Redis MULTI/EXEC (pipelined, ~0.5ms)
 
Query: "Find available drivers within 2km of rider"
1. Compute rider's H3 res9 cell
2. Get k-ring of radius ~3 (covers ~2km at res9)
3. SUNION all driver sets in those cells
4. Filter by status=AVAILABLE and eligible_ride_type
5. Result: 10-50 candidate driver IDs with positions
Latency: ~5-10ms for the spatial query

Deep Dive 2: Matching Algorithm (DISCO Core)

Three-phase matching pipeline:

When rider requests ride (total budget: <500ms p50, <2s p99):
 
PHASE 1 — CANDIDATE GENERATION (< 100ms):
Input: rider pickup H3 cell, ride_type
Algorithm:
ring = 0
candidates = []
while len(candidates) < 10 and ring <= MAX_RING (5km equivalent):
ring += 1
cells = h3.k_ring(pickup_cell, ring)
for cell in cells:
drivers = redis.smembers("drivers:{cell}")
for d in drivers:
if d.status == AVAILABLE and ride_type in d.eligible_types:
if not heading_away(d.heading, d.position, pickup):
candidates.append(d)
Filter out:
- Drivers heading away from pickup (>120° off bearing)
- Drivers with active offer pending
- Drivers who declined this rider in last 5 min (anti-loop)
Result: ~10-50 candidate drivers
 
PHASE 2 — SCORING (< 200ms, batched ETA call):
# Batch ETA request to Michelangelo for all candidates at once
etas = ETAService.batch_predict([{driver.pos → pickup} for driver in candidates])
 
For each candidate driver:
score = α × normalize(-eta_to_pickup) # shorter pickup time is better
+ β × normalize(-detour_from_route) # if driver has destination filter
+ γ × normalize(driver.rating) # higher rated drivers preferred
+ δ × normalize(fairness_boost) # drivers waiting longest get boost
+ ε × normalize(supply_health_impact) # don't drain supply from an area
 
Where (Phase 1 weights, tuned via A/B testing):
α = 0.40 (ETA dominates — riders care most about pickup time)
β = 0.10 (destination mode convenience for driver)
γ = 0.10 (quality signal)
δ = 0.25 (fairness is critical for driver retention)
ε = 0.15 (marketplace health — prevent supply deserts)
 
fairness_boost = log(1 + driver.consecutive_idle_seconds / 60)
supply_health_impact = -1.0 if removing driver creates supply desert in their cell
(i.e., cell drops below min_supply_threshold)
 
Sort candidates by score descending.
 
PHASE 3 — DISPATCH (< 50ms + 15s driver window):
selected = candidates[0] # highest score
offer = create_offer(trip_id, selected.driver_id, expiry=15s)
 
Push offer to driver via gRPC stream / push notification
Start 15-second timer
 
on ACCEPT:
transition trip to DRIVER_ASSIGNED
publish MATCH_COMPLETED event
notify rider with driver details + ETA
 
on DECLINE or TIMEOUT:
offer_sequence += 1
if offer_sequence <= 3:
selected = candidates[offer_sequence] # next best
repeat dispatch
else:
expand search radius by 2x
re-run Phase 1 with larger radius
if still no match after 60s total:
notify rider "No drivers available"
publish MATCH_FAILED event

Phase 2 ML Enhancement (via Michelangelo):

In Phase 2 of the project, the static weight-based scoring is replaced with an ML model that predicts P(successful_trip | driver, rider, context):

Features fed to ML model:
- Driver: location, heading, speed, rating, acceptance_rate, trips_today
- Rider: location, ride_type, historical rating, cancel_rate
- Context: time_of_day, day_of_week, weather, surge_multiplier, local_supply_count
- Historical: past driver-rider pair outcomes (if any)
 
Model predicts:
- P(driver accepts offer)
- P(trip completes without cancellation)
- Expected pickup ETA (DeepETA model)
- Expected rider satisfaction score
 
Combined into final score that optimizes for platform-wide trip completion rate.
Trained on Michelangelo, serving at 10M predictions/sec across all cities.

Deep Dive 3: UberPool (Shared Rides)

Multi-rider matching is significantly harder (NP-hard — dynamic vehicle routing problem):
 
Algorithm:
1. New rider requests Pool ride
2. Check active Pool trips within 3 H3 res9 rings of pickup
3. For each active Pool trip with available capacity:
- Compute route with new rider inserted at optimal point
- Calculate detour for existing rider(s):
detour_ratio = new_route_time / original_route_time
- Constraint: detour_ratio <= 1.50 (max 50% additional time)
- Vehicle capacity: max 2 Pool riders (3 for UberX Share)
4. Score each potential match:
system_score = saved_driver_minutes × driver_cost_per_min
+ saved_vehicle_miles × cost_per_mile
- detour_penalty × Σ(existing_rider_detour_minutes)
- new_rider_wait_penalty × additional_wait_minutes
5. If best match score > threshold → add rider to existing trip:
- Recalculate route for driver
- Notify existing rider(s) of slight detour
- Display shared savings to all riders
6. If no match → create new Pool trip:
- Dispatch single-rider (for now)
- Keep trip eligible for future rider matches for next 5 minutes
 
This is a variant of the Dynamic Vehicle Routing Problem (DVRP).
Uber uses heuristics + simulation, not exact solutions (NP-hard).
Real-time constraint: must decide within 30 seconds for rider experience.
 
Optimization trick — batch matching:
Instead of matching each Pool request individually, DISCO batches
Pool requests in 5-second windows and runs a global optimization:
maximize: total_system_efficiency (rides served / driver-hours)
subject to: max_detour <= 50%, max_wait <= 8 min, capacity <= limit
This batch approach yields ~15% better matches than greedy individual matching.

Deep Dive 4: Fairness in Dispatching

Problem: drivers near hotspots (airports, downtown) get all the rides;
drivers at edges get nothing. Without fairness, driver churn increases.
 
Fairness mechanisms:
 
1. Wait-time boost (δ weight in scoring):
- Driver idle for 10 min gets 2x fairness_boost vs driver idle 2 min
- Logarithmic decay: prevents extreme starvation but doesn't override ETA too much
- Tracked per-driver in Redis, reset on trip completion
 
2. Heat map (supply guidance):
- Demand Forecasting model (Michelangelo) predicts demand per H3 res7 cell
for next 15 min, updated every 5 min
- Driver app shows color-coded map: red = high demand, blue = oversupplied
- Drivers can choose to reposition — this is a nudge, not a mandate
 
3. Earning goals:
- If driver is within $10 of daily earning goal, slight priority boost
- Helps drivers reach psychological "session complete" threshold
- Reduces aimless driving and improves driver satisfaction
 
4. Anti-cherry-picking:
- Drivers cannot see ride destination before accepting
(prevents declining short/low-value rides)
- Acceptance rate below 80% triggers warning; below 60% triggers temporary deprioritization
- Exception: Diamond/Platinum drivers (high-tier) see destination as a perk
 
5. Surge pricing (market mechanism):
- Higher prices attract more supply to high-demand areas
- Surge multiplier computed per H3 res7 cell based on:
supply_demand_ratio = available_drivers / pending_requests
if ratio < 0.5: surge = 1.5 - 3.0x (graduated)
if ratio < 0.2: surge = 3.0 - 5.0x (extreme)
- Surge prices shown to rider before request confirmation
- Revenue split: surge premium goes to driver (incentive to reposition)
 
6. Geographic equity zones:
- Some neighborhoods historically underserved
- System maintains minimum supply targets per zone
- If zone drops below target, nearby drivers get bonus for accepting rides there

Deep Dive 5: Ringpop-Based Sharding and Consistency

Problem: DISCO must maintain an in-memory driver index for fast matching.
A single machine cannot hold all 4.4M online drivers. We must shard.
 
Solution: Ringpop (Uber's open-source consistent hashing library)
 
Architecture:
- Each DISCO instance owns a set of H3 resolution 4 cells (~1,770 km² each)
- Ringpop consistent hash ring maps H3 res4 cell → DISCO instance
- All driver updates for drivers in that cell route to the owning instance
- All ride requests for pickups in that cell route to the same instance
- Co-location: driver index + matching logic on same node = zero network hop for lookups
 
Membership protocol:
- SWIM gossip protocol (Scalable Weakly-consistent Infection-style)
- Availability over correctness (AP): a driver may briefly appear in 2 shards
during rebalance — this causes duplicate offers at worst (handled by Offer Manager
with idempotency)
- Failure detection: <10 seconds to detect dead node via gossip
- Rebalancing: Ringpop automatically redistributes cells to surviving nodes
 
Scaling:
- ~500 DISCO instances globally
- Each instance handles ~8,800 online drivers (4.4M / 500)
- Each instance handles ~2.4 match requests/sec (1200 peak / 500)
- Memory per instance: ~8,800 drivers × 128 bytes ≈ 1.1MB (trivial)
- CPU per instance: dominated by ETA batch calls and scoring
 
Cross-shard matching:
- Rider near a shard boundary (H3 res4 cell edge):
- Expanding ring search may need candidates from adjacent shard
- DISCO instance makes gRPC call to neighboring shard's DISCO instance
- Adds ~10ms latency but rare (only for riders near cell edges)
- Alternative: overlap zones where drivers are indexed in 2 shards

Deep Dive 6: Disaster Recovery (Driver-Side State Digest)

Problem: If a data center hosting DISCO fails, all in-memory driver state is lost.
Rebuilding from Kafka replay takes minutes — too slow for a real-time system.
 
Uber's clever solution: encrypted state digest on driver's phone
 
1. Every 30 seconds, DISCO sends an encrypted state digest to the driver app:
digest = encrypt({
driver_id, status, current_trip_id, h3_cell,
last_known_rider_id, trip_state, route_progress
})
 
2. Driver app stores digest in local storage (< 1KB)
 
3. On data center failover:
a. Backup DISCO instance comes online
b. Sends "state recovery" request to all driver apps in region
c. Drivers send back their encrypted state digests
d. Backup DISCO decrypts and rebuilds in-memory state
e. Recovery time: ~30 seconds (vs minutes for Kafka replay)
 
4. Correctness guarantee:
- State may be up to 30 seconds stale
- Active trips continue (driver knows their current trip)
- In-flight match offers may be lost (rider retries automatically)
- Supply index rebuilt within 1 GPS cycle (4 seconds) from live updates
7

Multi-team rollout

Migration Strategy: Legacy DISCO → H3-Based DISCO v2

Since Uber’s existing dispatch system uses S2 cells and simpler nearest-driver matching, the migration to H3-based multi-factor matching must be carefully staged.

Phase 1: Shadow Mode (Weeks 1-6)

  • Deploy DISCO v2 alongside legacy DISCO in 3 pilot cities (e.g., San Francisco, Sao Paulo, Bangalore — diverse traffic patterns)
  • Both systems receive every ride request; legacy DISCO makes the actual dispatch decision
  • DISCO v2 runs full matching pipeline but only logs its decision (no driver impact)
  • Compare: match quality (ETA accuracy, driver idle time), latency (p50/p99), candidate overlap
  • Validate H3 indexing correctness against S2-based results
  • Metrics dashboard: side-by-side comparison per city

Phase 2: A/B Test (Weeks 7-14)

  • 5% of ride requests in pilot cities routed to DISCO v2 for actual dispatch
  • A/B test metrics: rider wait time, driver earnings/hour, cancellation rate, match latency
  • Ramp: 5% → 10% → 25% → 50% over 4 weeks per city
  • DISCO v2 must show statistical improvement (or no regression) on all primary metrics before ramp
  • On-call rotation: DISCO team staffs dedicated on-call for v2 during rollout

Phase 3: City-by-City Rollout (Weeks 15-26)

  • After pilot cities validated, roll out to remaining cities in tiers:
  • Tier 1: Top 20 cities by trip volume (weeks 15-18)
  • Tier 2: Cities 21-200 (weeks 19-22)
  • Tier 3: All remaining cities (weeks 23-26)
  • Each tier: 1 day at 10%, 2 days at 50%, then 100%
  • Legacy DISCO remains deployed (cold standby) for 4 weeks after each tier completes

Phase 4: Legacy Decommission (Months 7-9)

  • Remove legacy DISCO traffic routing
  • Migrate S2-based data pipelines to H3
  • Decommission legacy DISCO instances and S2 index infrastructure
  • Archive legacy code (don't delete — may need for forensic debugging)

Rollback Plan

Stage
Rollback Action
Data Impact
Phase 1 (Shadow)
Disable DISCO v2 logging
None — v2 never dispatched
Phase 2 (A/B)
Feature flag routes 100% to legacy
None — trips already completed are unaffected
Phase 3 (City rollout)
Per-city feature flag back to legacy DISCO
Active trips stay on v2 until completed; new requests go to legacy
Phase 4 (Decommission)
Cannot roll back (legacy decommissioned)
Must fix forward; rebuild legacy from cold standby if catastrophic

Cross-Team Coordination

Team
Phase 1 Role
Phase 2+ Role
DISCO Team
Build v2, shadow pipeline, comparison dashboard
A/B test analysis, on-call, city rollouts
Geospatial Team
H3 library integration, index migration tooling
Monitor H3 query performance at scale
Supply Team
No change (both DISCOs use same Supply Service)
Validate driver experience metrics
ML Platform
Deploy ETA model for v2, feature parity
Retrain models on v2 dispatch data
Marketplace/Pricing
No change (surge pricing independent)
Validate surge response with new matching patterns
Data Platform
Dual logging pipeline for A/B comparison
Migrate analytics from S2 to H3 dimensions
8

Bottlenecks & evolution

Bottleneck Analysis

Component
Bottleneck
Mitigation
GPS ingestion (Kafka)
2M msg/sec write throughput; topic partition hotspots in dense cities
Partition by H3 res4 cell (geographic distribution); 500+ partitions; compress with Snappy
Driver index (Redis)
Thundering herd on popular H3 cells (airports, stadiums after events)
Pre-warm popular cells; rate-limit index reads with local caching on DISCO instances (100ms TTL)
ETA Service
Batch prediction latency spikes under load; model serving GPU saturation
Request coalescing (batch multiple DISCO requests); model distillation for smaller/faster models; CPU fallback model
DISCO Ringpop rebalance
Cell handoff during node failure causes 5-10s gap in that cell's matching
Overlap period: both old and new owner accept requests during handoff; idempotent offer creation prevents duplicates
Offer delivery (push)
Driver on poor cellular network; push notification delays up to 5s
Redundant channels (FCM + persistent gRPC stream); pre-establish connection before match; timeout tuned per region
Cross-shard queries
Riders near H3 res4 cell boundaries need candidates from 2 shards
Overlap zones: index drivers in primary + adjacent shard if within 500m of boundary; adds ~10% index overhead

Observability

Key metrics (RED method):

Service
Rate
Errors
Duration
DISCO (matching)
Matches/sec, offers/sec
Match failures, offer timeouts, cascade rate
Match latency p50/p99, time-to-first-offer
Supply Service
Location updates/sec, drivers online
Stale location count, index write failures
Index update latency, query latency
ETA Service
Predictions/sec, batch size
Prediction timeouts, model errors
Prediction latency p50/p99
Offer Manager
Offers sent/sec, accepts/sec
Push delivery failures, expired offers
Offer delivery latency, driver response time
Kafka Pipelines
Messages/sec per topic, consumer lag
Consumer failures, deserialization errors
End-to-end lag (produce to consume)

Critical alerts:

1. Match latency p99 > 2s — Matching SLA violated. Investigate: ETA service latency, Redis query time, candidate pool size. Auto-remediation: fall back to nearest-driver matching (skip ML scoring).

2. Match failure rate > 5% — Too many ride requests ending in “No drivers available.” Investigate: supply levels per city, surge pricing response, driver app GPS issues.

3. Kafka consumer lag > 30s on supply.location-updates — Driver positions becoming dangerously stale. Investigate: consumer throughput, partition imbalance. Auto-remediation: scale consumers, alert on-call.

4. Ringpop membership churn > 10 nodes/min — Cluster instability causing constant rebalancing. Investigate: network partitions, host health. Auto-remediation: freeze rebalancing (pin assignments) until stabilized.

5. Offer cascade rate > 40% — More than 40% of first-choice drivers declining. Investigate: offer quality (are we picking distant drivers?), driver incentive issues, app push delivery issues.

Distributed tracing:

  • Trace from rider tap "Request" → API Gateway → Trip Service → DISCO (candidate generation → scoring → dispatch) → Offer Manager → Driver App → Accept → back to Rider
  • Include span tags: city_id, h3_cell, candidate_count, search_radius, algorithm_version, cascade_attempt
  • Trace sampling: 100% for match failures, 10% for successful matches, 0.1% for location updates
  • Tool: Jaeger (Uber open-sourced this for exactly this use case)

2-3 Year Evolution

Year 1: Foundation — H3-Based Matching with ETA Optimization

Core DISCO v2 operational across all 10,000+ cities with H3-based geospatial indexing replacing legacy S2 cells. Multi-factor scoring (ETA, fairness, supply health) active for all ride types. DeepETA model integrated via Michelangelo reduces rider wait time by 10-15% vs distance-only matching. Fairness mechanisms (wait-time boost, supply health scoring) reduce driver earnings variance by 20%. Disaster recovery via driver-side state digest operational with <30s recovery time. Operational maturity: comprehensive Jaeger tracing, RED metrics dashboards per city, automated runbooks for top 10 incident types, on-call rotation with 15-min response SLA.

Year 2: ML-Powered Matching, Dynamic Pricing, Multi-Modal

Replace static scoring weights (alpha/beta/gamma) with ML model that predicts P(successful_trip) per driver-rider pair. Model trained on billions of historical trips via Michelangelo, features include real-time traffic, weather, driver fatigue signals, rider behavioral patterns. Integrate dynamic pricing directly into matching: surge multiplier influences which drivers are shown to which riders (higher-surge rides preferentially offered to drivers in low-supply areas, creating natural rebalancing). UberPool batch matching optimization: 5-second batching windows with global optimization yield 15% improvement in shared ride efficiency. Multi-modal trip planning: for trips where transit + Uber is faster/cheaper, suggest combined itinerary (e.g., “Take subway to downtown, Uber last mile”). Begin work on unified Fulfillment Platform abstractions so same matching engine can serve Rides, Eats, and Freight with product-specific scoring plugins.

Year 3: Autonomous Vehicles, Predictive Positioning, Unified Matching

Autonomous vehicle (AV) fleet integration: AVs have no “driver acceptance” step — matching is deterministic and instant. AV scoring includes battery/fuel level, passenger pickup capability, maintenance schedule. Mixed fleet matching: optimize human driver + AV allocation per area based on ride type, time of day, and AV coverage zones. Predictive supply positioning: demand forecasting model (trained on 3 years of trip data, weather, events, holidays) pre-positions drivers 15-30 min before demand spikes. Instead of reactive surge pricing, proactively nudge supply to match predicted demand. Unified matching engine live across Rides, Eats, and Freight: a single driver can be offered a ride OR a food delivery based on which maximizes platform-wide efficiency. Cross-product matching reduces driver idle time by an estimated 20%.

Cost Optimization Roadmap

Strategy
Timeline
Savings
Binary protobuf for GPS (replace JSON)
Month 2
40% on ingestion bandwidth (~$160K/mo)
GPS update batching on driver app (batch 3 updates, send every 12s)
Month 4
66% fewer Kafka messages (~$145K/mo)
Tiered Redis: hot cells in Redis, cold cells in Cassandra
Month 6
30% on Redis cluster cost (~$22K/mo)
ETA model distillation (smaller model, CPU-only serving)
Month 9
50% on ML serving cost (~$90K/mo)
Reserved instances for baseline DISCO + Kafka capacity
Month 3
30% on committed compute (~$110K/mo)
DISCO instance right-sizing (some cities need <1 full instance)
Month 12
20% on DISCO compute (~$29K/mo)

Summary

The key staff-level insights in this design:

1. H3 hexagonal grid is the foundational data structure — Uber built it specifically because hexagons provide equidistant neighbors, eliminating edge artifacts that plague square/rectangular grids in expanding-ring driver searches. The resolution hierarchy (res4 for sharding, res7 for pricing zones, res9 for driver indexing) maps cleanly to different system concerns.

1. Ringpop consistent hashing co-locates data and compute — By sharding DISCO instances by H3 res4 cell, every matching operation hits a local in-memory driver index with zero network hops. This is why matching can complete in <500ms despite evaluating 10-50 candidates with ML-based scoring.

1. Fairness is a first-class architectural concern, not an afterthought — The 25% weight on fairness_boost in scoring, combined with anti-cherry-picking rules and supply health scoring, directly addresses driver retention — the biggest operational risk in a two-sided marketplace. Without fairness, top drivers cluster at hotspots, edge-area drivers churn, and supply collapses.

1. Driver-side state digest is an elegant disaster recovery pattern — Pushing encrypted state to the entity that is already distributed (millions of driver phones) avoids the cold-start problem of rebuilding from Kafka logs. Recovery in ~30 seconds vs minutes from log replay.

1. The matching pipeline degrades gracefully — Each failure domain has an independent degradation mode: stale GPS positions are still useful, ETA service failure falls back to distance heuristics, ML scoring failure falls back to static weights, push delivery failure triggers automatic cascade to next driver. The system never hard-fails on a single component.

1. Migration via shadow mode + A/B testing + city-by-city rollout reflects the reality that you cannot do a big-bang cutover for a system processing 36M trips/day — the strangler fig pattern with per-city feature flags gives confidence at each stage while keeping legacy as a safety net.

Rubric — Senior vs Staff

Dimension
Senior signal
Staff signal
Geospatial indexing
Picks a grid index and runs a radius search for nearby drivers.
Weighs geohash / S2 / H3; justifies hexagonal equidistant neighbors and a res4/7/9/12 hierarchy.
Matching algorithm
Nearest-driver by ETA with an accept/decline retry.
Three-phase generate-score-dispatch with weighted scoring, offer cascade, and radius expansion.
Sharding & state
Shards the driver index horizontally by region.
Consistent-hash (gossip-membership) sharding by geo cell, co-locating data and compute; handles cell-edge queries.
Fairness & marketplace health
Mentions fairness but leaves it outside the core design.
Bakes wait-time boost, anti-cherry-picking, and supply-desert avoidance into scoring to curb driver churn.
Failure & recovery
Adds replicas and retries for availability.
Independent per-domain degraded modes plus driver-side state digest for sub-30s region recovery.
★ MORE WALKTHROUGHS

Want more breakdowns like this?

Join free early access for upcoming RAG, LLM eval, agents, and AI infrastructure walkthroughs.

Join Free Early Access →