system-design/design-uber.md

35. Design Uber / Ride Hailing

Tests: real-time location, geo-indexing (S2 / H3), matching, dispatch, payments, surge pricing, websockets at scale.

~4 min read·updated 5/29/2026

35. Design Uber / Ride Hailing

Tests: real-time location, geo-indexing (S2 / H3), matching, dispatch, payments, surge pricing, websockets at scale.

35.1 Requirements

Functional

  • Driver app: broadcast location.
  • Rider app: request a ride; get matched with a driver.
  • Real-time tracking during the ride.
  • Pricing (surge in high-demand areas).
  • Payment after trip.
  • Trip history.

Non-functional

  • Real-time location updates (~5 sec).
  • Low-latency matching (P95 < 5 sec from request to driver assignment).
  • High availability (lost rides cost money).
  • Geographically distributed (many cities, many regions).
  • Scale: 100M+ users, 5M+ active drivers, millions of trips/day, tens of thousands of QPS for location updates.

35.2 Estimates

  • 5M active drivers × 1 location/4 sec = ~1.25M location updates/sec peak. Huge.
  • Trips: ~10M/day = ~120/sec average; peak 1K/sec.
  • Storage of location: ephemeral (no need for full history at scale; sample for analytics).

35.3 High-level architecture

[ Driver app ]──ws──→[ Location ingest ]──→[ Geo-index (per region) ]
[ Rider app ]──→[ Trip svc ]──→[ Match svc ]←─[ Geo-index ]
                       │
                  [ Trip DB ]
                       │
                  [ Pricing svc ]──→[ Surge model ]
                  [ Payments svc ]
                  [ Notification svc ]──→[ Driver app + Rider app ws ]

35.4 Location updates

Drivers stream location every ~4 sec. Aggregated:

  • WebSocket to nearest gateway region.
  • In-memory cache: driver_id → (lat, lon, ts, status).
  • Persisted (briefly) for matching; not kept long term.

Volume: 1M+ updates/sec → distribute across many ingest servers, partitioned by driver_id.

35.5 Geo indexing: the central problem

To find "nearest driver to a rider," you need a spatial index optimized for "find drivers within R meters."

Approaches

R-tree (in PostGIS): bounding-box hierarchies. Works great in a single DB; doesn't shard well.

Geohash: encode lat/lon into a string. Nearby points share prefix. Range scan covers nearby cells. Issues at cell boundaries.

Google S2: hierarchical decomposition of the sphere into cells. Each cell has a 64-bit ID. Used in Google Maps, Foursquare.

Uber H3: hexagonal hierarchical index. Hexagons have uniform distance to neighbors (no diagonal vs straight problem). Used in Uber's Marketplace.

Why hexagons (H3)

  • Each hex has 6 equidistant neighbors (squares have 8 with diagonal further).
  • Cell sizes uniform.
  • "k-ring" query: get all hexes within k steps — natural for "find drivers in 1km radius."

Architecture

  • Each city/region has its own geo-index instance.
  • Drivers in a city register their hex cells.
  • Match request: get rider's hex; query nearby hexes; return drivers.

In-memory KV store (Redis with geo commands, or custom built on H3).

35.6 Trip request → match flow

1. Rider app: POST /trips/request {pickup, destination}
2. Trip svc: create trip in PENDING state; emit MatchRequested event
3. Match svc: 
   - Convert pickup to H3 cell
   - Query nearby drivers (geo-index)
   - Filter: status=AVAILABLE, vehicle_type matches, in service area
   - Score: distance, ETA, driver rating, etc.
   - Top N candidates
4. For each (in order):
   - Send offer to driver (WebSocket push) with timeout (10-30 sec)
   - On accept: assign driver to trip; notify rider
   - On reject/timeout: try next candidate
5. If no driver matches in T seconds: expand radius, raise surge, try again
6. If still no match: notify rider "no driver available"

Match service is stateful per region; sharded by city.

35.7 Live tracking

Once matched:

  • Driver app sends location to Trip svc.
  • Trip svc broadcasts to Rider app via WebSocket.
  • ETA updated by routing service (computes based on traffic).

35.8 Routing & ETA

Map data + real-time traffic.

  • OSRM / Valhalla (open source).
  • Google Maps / Mapbox APIs.
  • ML-based ETA with historical patterns.

For scale: precomputed graph (highway hierarchy) + contraction hierarchies for fast routing queries.

35.9 Pricing & surge

Base price + per-mile + per-min + service fee.

Surge multiplier:

  • Per region (small geofences, e.g., H3 cells).
  • Continuously updated based on supply/demand.
  • Stream processor (Flink): consumes ride requests + completed matches; updates per-cell surge factor.

When rider requests during surge: app shows multiplier upfront for confirmation.

35.10 Payments

After trip ends:

  • Compute fare (actual time + distance + surge applied at request time).
  • Charge rider's saved payment method (tokenized; via Stripe / Adyen / Braintree).
  • Record transaction.
  • Pay driver (delayed payout, batched).

Payment service: own DB; idempotency keys per trip; webhooks from PSP for status.

35.11 Notifications

  • Push (APNS / FCM) for "driver is here."
  • WebSocket for live updates within app.
  • SMS fallback if app not active.

35.12 Trip storage

Trips persist long-term:

  • trips: trip_id, rider_id, driver_id, pickup, dropoff, fare, status, timestamps.
  • Sharded by trip_id or by city.
  • Postgres or distributed SQL (Spanner-class for cross-shard analytics).

35.13 Multi-region / multi-city

Each city is largely independent. Architecture per city:

  • Local Match svc.
  • Local geo-index.
  • Local payment processing (regional).
  • Shared user / driver identity.

Failover within a region (multi-AZ); cross-region failover only for catastrophic events.

35.14 Cell-based architecture

Uber uses cell-based architecture (similar to AWS): each city is a cell with its own stack. Failures contained. Promotes a new city = spin up a cell.

35.15 Failure modes

  • Match svc down for a city: rides fail; manual fallback (call dispatcher).
  • Geo-index lag: drivers shown as available who aren't, or vice versa.
  • Payment failure: charge later (debt collection workflow), retry, fall back to cash if allowed.
  • WebSocket gateway crash: reconnect to another; map of (driver_id → gateway) regenerates.

35.16 Scaling chokepoints

  • WebSocket gateway: tens of millions of long-lived connections. Needs horizontal scale.
  • Location ingest: partitioning by driver_id; each partition handles a fraction.
  • Match svc: per-city sharding; biggest cities get dedicated clusters.
  • Counter / surge updates: stream processor must keep up.

35.17 ML in the loop

  • ETA prediction.
  • Ranking driver candidates (probability of accept, completion).
  • Surge forecasting.
  • Fraud detection (collusion between driver and rider).

Feature store (online + offline) provides features at request time.

35.18 Sketch for interview

[Driver app]──ws──→[Loc ingest]──→[Geo-index (H3)]
[Rider app]──→[Trip svc]──→[Match svc]──→[Geo-index]
                              ↓
                        candidate offers via ws to drivers
                              ↓
                       chosen driver → trip ACTIVE
[Trip svc]──→[Routing/ETA svc]
[Trip svc]──→[Pricing svc]──→[Surge model from Flink]
[Trip svc]──→[Payments svc]──→[PSP]
[Notif svc]──→[APNS/FCM, ws]
[Trip DB] sharded by trip_id; per-city analytics

35.19 What an interviewer wants

  • Geo-index with H3 / S2 / geohash and rationale (hex preferred).
  • WebSocket for real-time bidirectional driver/rider updates.
  • City-based partitioning.
  • Match algorithm: nearby + filter + score + offer-with-timeout.
  • Idempotent payments with tokenization.
  • Stream-driven surge.

Key takeaways

  • Geo-index is the central data structure (H3 / S2). Per-city sharding.
  • Real-time pipeline: drivers stream location → ingest → in-memory index → match.
  • Match: nearby candidates + offer-with-timeout + retry / radius expansion.
  • Surge: stream processing in real-time per cell.
  • Cell-based architecture per city limits blast radius.
  • WebSocket scale: millions of concurrent connections; partitioned gateways.

// 2 views

main
UTF-8·typescript