Case Studies

Design Uber

● Advanced ⏱ 15 min read case-study

Uber is a real-time marketplace that matches riders and drivers across 10,000 cities. The defining technical challenges are geospatial — tracking millions of moving drivers, finding the closest available driver for any rider, and doing it all in under a second. Uber also introduced surge pricing, which requires near-real-time supply/demand analysis over geographic regions. Designing Uber brings together location tracking, geospatial indexing, real-time communication, and marketplace coordination.

Requirements

Functional:

Non-functional:

Capacity Estimation

Daily trips: 15M
Peak trips per second: ~5,000 concurrent trip requests
Active drivers: 5M globally
Location updates: 5M drivers × 1 update/4s = 1.25M location updates/second
Riders actively tracking driver: 500K concurrent websocket connections

Location storage: 5M × 1 update/4s × 50 bytes = 62.5 MB/s
Only current location needs to be queryable fast — history can be async

Driver Location Tracking

Driver location is the foundation of everything — matching, ETAs, and live tracking all depend on having accurate, fresh driver positions.

Location update pipeline:

  1. Driver app sends GPS coordinates every 4 seconds via WebSocket or HTTP POST.
  2. Location update hits a location service, which writes to Redis: driver:{driver_id} → {lat, lng, timestamp, status} with a TTL of 30 seconds (if a driver stops sending updates, they age out of the active pool).
  3. The location is also indexed in a geospatial index for proximity queries.
  4. For trips in progress, the location is forwarded to the rider’s WebSocket connection for live tracking.

Geospatial indexing for proximity: To find the nearest driver to a pickup location, the system needs a geospatial index. Options:

Uber architecture: location ingestion, geospatial indexing, matching service, and real-time trip tracking

Ride Matching

When a rider requests a ride, the matching service finds the best available driver:

  1. Rider submits request with pickup location and desired vehicle type.
  2. Matching service queries the geospatial index for available drivers within radius R (starting at 1km, expanding if needed).
  3. For each candidate driver, compute the estimated time of arrival (ETA) from the driver’s current location to the pickup — not straight-line distance, but actual drive time on the road network.
  4. Rank candidates by ETA (and optionally other factors: rating, acceptance rate, trip history with the rider).
  5. Offer the ride to the top-ranked driver. If the driver accepts within N seconds, the match is confirmed. If they decline or time out, offer to the next candidate.

Preventing double-dispatch: Two riders must not be matched to the same driver simultaneously. The matching service uses an optimistic locking pattern: when offering a ride to a driver, it atomically sets a driver_status = offered flag with a TTL (e.g., 10 seconds). If the driver accepts, status transitions to on_trip. If another ride offer tries to set the flag while it’s already offered, it fails — the driver is skipped. Redis atomic operations (SET NX EX) implement this without distributed locking overhead.

Dispatch & Trip Lifecycle

Once matched, the trip moves through a state machine:

REQUESTED → MATCHED → DRIVER_EN_ROUTE → ARRIVED → IN_PROGRESS → COMPLETED
                                                               ↘ CANCELLED

Each state transition is persisted to the trip database (Schemaless — Uber’s MySQL-backed distributed datastore) and published to a Kafka topic. Services that need trip events (billing, notifications, driver payout) subscribe to the topic independently.

Real-time tracking during the trip: Both rider and driver apps maintain WebSocket connections to the trip service. As the driver moves, their location updates are forwarded to the rider’s WebSocket. The driver sees the rider’s pickup pin; the rider sees the driver’s moving icon. Connection management uses the same chat-server-style architecture: a connection registry maps user ID → WebSocket server, and servers forward location events between each other.

Surge Pricing

Surge pricing (dynamic pricing multipliers) increases fares when demand exceeds supply in a geographic area. It serves two purposes: incentivizes more drivers to go online, and reduces demand by discouraging non-urgent trips.

Computing surge:

  1. Divide the city into geographic cells (H3 hexagons, typically ~1km diameter).
  2. For each cell, track: active drivers, open ride requests, historical conversion rate.
  3. Compute supply-demand ratio per cell: surge_factor = f(open_requests / available_drivers). When requests far outnumber drivers, the multiplier rises.
  4. Smooth over time and adjacent cells (a 3× surge in one cell with 1× in the adjacent cell would send drivers a block away).
  5. Apply the multiplier to fare estimates in the affected cells. Show the surge map and multiplier to both riders and drivers in real time.

The surge computation runs as a streaming job (Kafka + Flink) — processing location events and ride requests continuously, updating cell surge values every 30–60 seconds. Results are cached in Redis per cell for fast read access during fare estimation.

Maps & ETA

Uber needs accurate ETAs for matching, fare estimation, and rider/driver communication. Computing ETAs requires a road network graph and real-time traffic data.

Scaling Considerations