Full Stack • Java • System Design • Cloud • AI Engineering

Uber System Design — 1 Hour Interview Guide

Design a scalable ride-hailing platform like Uber. Covers requirements, capacity estimation, high-level architecture, driver-rider matching, geospatial indexing (Geohash, H3, Quadtree), real-time location tracking, ETA computation, surge pricing, WebSocket tracking, database design, and failure scenarios — all in a 1-hour interview format.

1-Hour Interview Roadmap

Time Topic
0 – 5 min Requirements clarification
5 – 10 min Capacity estimation
10 – 18 min High-level architecture + core components
18 – 28 min Ride booking flow + driver matching
28 – 38 min Nearby driver search — Geohash, Quadtree, H3
38 – 46 min Real-time location tracking + ETA computation
46 – 52 min Database design + caching
52 – 57 min Surge pricing + payments + ratings
57 – 60 min Trade-offs + failure scenarios

What Are We Building?

A ride-hailing platform where:

  • Riders can request rides, get fare estimates, track their driver in real time, pay, and rate the trip
  • Drivers can go online, receive ride requests, accept or decline, navigate to the pickup, and get paid

Scale reference: Uber operates in 70+ countries, with 137 million monthly active users, 5.4 million active drivers, and processes ~19 million trips per day at peak.

Key unique challenges:

  • Geospatial matching — find the nearest available driver in milliseconds across millions of concurrent GPS updates
  • Real-time location tracking — ~33,000 driver location writes per second
  • Surge pricing — dynamic fare multiplier computed from live supply vs demand
  • Ride state machine — a ride transitions through 6+ states; any state corruption = money lost or rider stranded

Step 1 — Requirements

Functional Requirements

# Requirement
1 Rider can enter pickup and destination, see fare estimate + ETA, then request a ride
2 System matches the rider with the nearest available driver
3 Driver can accept or decline a ride request within 30 seconds
4 Rider can track the driver's real-time location on a map
5 System navigates the driver to the pickup and then to the destination
6 Rider is charged automatically at ride completion
7 Rider and driver can rate each other after the ride
8 Driver can go online/offline; system only dispatches to online drivers
9 System applies surge pricing when demand significantly exceeds supply
10 System handles ride cancellations (before and after driver assignment)

Non-Functional Requirements

# Requirement
1 Driver-rider match must complete in < 5 seconds
2 Location updates must propagate with < 1 second latency
3 High availability — 99.99% uptime; a rider should never be left without a match
4 System must handle 5,000 new ride requests per second at peak
5 Location data storage is eventually consistent — last known position is sufficient
6 Payment and ride history must be strongly consistent
7 System must scale horizontally for peak events (New Year's Eve, concerts)

Out of Scope

  • Driver background check and onboarding
  • Rider payment method management and fraud detection
  • Navigation turn-by-turn (uses Google Maps / Mapbox API)
  • Driver earnings dashboard
  • Pooled rides (UberPool)

Step 2 — Capacity Estimation

Assumptions

Metric Value
Total riders 137 million
Total drivers 5.4 million
Daily Active Riders 10 million
Daily Active Drivers 1 million
Peak concurrent riders 1 million
Peak concurrent active drivers 100,000
Daily ride requests 19 million
Peak new ride requests per second ~5,000 RPS
Driver location update frequency every 3 seconds

Location Update Volume

100,000 active drivers × 1 update / 3 sec = ~33,333 location writes/sec

Each update payload:
  { driver_id, lat, lon, heading, timestamp } ≈ 50 bytes

Ingest bandwidth: 33,333 × 50 bytes = ~1.6 MB/sec
Redis GEOADD throughput: well within limits for a 10-node cluster

Storage Estimation

Ride record:     ~200 bytes per ride
Daily rides:     19M × 200 bytes  = ~3.8 GB / day
Annual rides:    ~1.4 TB / year (ride history)

Driver profiles: 5M × 5 KB        = ~25 GB
Rider profiles:  137M × 2 KB      = ~274 GB

Location cache:  100K drivers × 50 bytes = ~5 MB (fits entirely in Redis)

API Traffic

Ride requests:          ~5,000 RPS
Location updates:       ~33,333 RPS
Rider location polls:   ~5,000 RPS (or WebSocket pushes)
Total peak API load:    ~45,000 RPS

Step 3 — High-Level Architecture

flowchart TD
    RiderApp[Rider App] --> AG[API Gateway\nAuth + Rate Limiting]
    DriverApp[Driver App] --> AG

    AG --> US[User Service]
    AG --> DS[Driver Service]
    AG --> RS[Ride Service]
    AG --> LS[Location Service]
    AG --> SRCH[Matching Service]

    RS --> KF[Kafka\nRide Events]
    KF --> PAY[Payment Service]
    KF --> RATE[Rating Service]
    KF --> NOTIF[Notification Service]

    RS --> ROUTE[Routing Service\nGoogle Maps / Mapbox]
    RS --> PRICE[Pricing Service\nSurge + Fare]

    LS --> GEO[(Redis\nGeoSpatial Index)]
    SRCH --> GEO

    RS --> RDB[(PostgreSQL\nRide History)]
    US --> UDB[(PostgreSQL\nUsers + Drivers)]
    PAY --> PAYDB[(PostgreSQL\nPayments)]

    RS --> WS[WebSocket Server\nReal-time Tracking]
    WS --> RiderApp

Component Responsibilities

Component Responsibility
API Gateway JWT auth, rate limiting, routing to correct microservice
User Service Rider + driver profile, registration, auth tokens
Driver Service Driver online/offline status, vehicle details, availability
Ride Service Ride lifecycle — create, assign, start, complete, cancel
Location Service Ingest and store real-time driver GPS; expose nearby driver query
Matching Service Find nearest available driver for a ride request
Routing Service Route calculation, ETA — calls Google Maps / Mapbox API
Pricing Service Fare estimation, surge multiplier based on supply vs demand
Payment Service Charge rider at ride completion via Stripe / PayPal
Rating Service Collect and store post-ride ratings for both parties
Notification Service Push notifications — driver assigned, ride started, receipt
WebSocket Server Push real-time driver location updates to rider app

Step 4 — Ride Booking Flow

End-to-End Ride Request Sequence

sequenceDiagram
    participant Rider
    participant AG as API Gateway
    participant RS as Ride Service
    participant PRICE as Pricing Service
    participant ROUTE as Routing Service
    participant MATCH as Matching Service
    participant LS as Location Service
    participant Driver

    Rider->>AG: POST /rides/estimate {pickup, destination}
    AG->>PRICE: GET fare estimate
    AG->>ROUTE: GET ETA
    AG-->>Rider: {fare_range: "$12-$15", eta_min: 4}

    Rider->>AG: POST /rides {pickup, destination, payment_method}
    AG->>RS: Create ride {status: REQUESTED}
    RS->>MATCH: Find nearest driver {pickup_lat, pickup_lon}
    MATCH->>LS: GET drivers within 5 km
    LS-->>MATCH: [driver_1, driver_2, driver_3] sorted by distance

    MATCH->>Driver: Push notification: ride request
    Driver->>MATCH: Accept ride (within 30s)
    MATCH->>RS: Assign driver_1 to ride
    RS->>RS: UPDATE ride status = DRIVER_ASSIGNED
    RS-->>Rider: {driver_id, vehicle, eta_sec: 240}

    Note over Driver,Rider: Driver navigates to pickup
    Driver->>RS: POST /rides/{id}/start
    RS->>RS: UPDATE status = IN_PROGRESS
    Driver->>RS: POST /rides/{id}/complete
    RS->>RS: UPDATE status = COMPLETED
    RS->>KF: Publish ride.completed event

Ride State Machine

stateDiagram-v2
    [*] --> REQUESTED : Rider submits request
    REQUESTED --> SEARCHING : Matching starts
    SEARCHING --> DRIVER_ASSIGNED : Driver accepts
    SEARCHING --> CANCELLED : No driver found / rider cancels
    DRIVER_ASSIGNED --> IN_PROGRESS : Driver starts ride
    DRIVER_ASSIGNED --> CANCELLED : Driver or rider cancels
    IN_PROGRESS --> COMPLETED : Driver ends ride
    COMPLETED --> [*]
    CANCELLED --> [*]

Driver Decline Cascade

If the selected driver declines or does not respond within 30 seconds:

1. Matching Service marks driver_1 as "declined this ride"
2. Select driver_2 from the ranked list
3. Push notification to driver_2
4. Repeat up to 5 candidates
5. If all 5 decline → expand search radius from 5 km → 10 km
6. If still no driver → return "no drivers available" to rider

Step 5 — Finding Nearby Drivers

This is the most technically demanding part of the Uber design. The system must answer the question:

"Give me all available drivers within 5 km of this coordinate, sorted by distance, in < 100ms."

At 100,000 concurrent active drivers updating location every 3 seconds, the naive SQL approach fails completely.

Approach 1 — Naive SQL (Do Not Use)

-- ❌ Full table scan — O(n) for every request
SELECT driver_id,
       (6371 * acos(cos(radians(37.7749)) * cos(radians(lat))
        * cos(radians(lon) - radians(-122.4194))
        + sin(radians(37.7749)) * sin(radians(lat)))) AS distance_km
FROM drivers
WHERE is_available = true
HAVING distance_km < 5
ORDER BY distance_km;

Problem: Full table scan on every ride request. With 1 million drivers, this is unacceptably slow.

Approach 2 — PostGIS (Better, Still Not Enough)

-- ✅ Better — uses spatial index (R-tree)
SELECT driver_id,
       ST_Distance(location::geography,
                   ST_MakePoint(-122.4194, 37.7749)::geography) AS dist_meters
FROM drivers
WHERE is_available = true
  AND ST_DWithin(location::geography,
                 ST_MakePoint(-122.4194, 37.7749)::geography, 5000)
ORDER BY dist_meters;

Problem: Works at medium scale but requires frequent index updates (location changes every 3 seconds). PostgreSQL write locks slow down under 33K writes/sec.

Approach 3 — Geohashing + Redis (Production Ready)

Geohashing converts a (lat, lon) coordinate into a fixed-length string. Nearby locations share the same prefix.

San Francisco: (37.7749, -122.4194) → geohash "9q8yy"
1 km away:     (37.7840, -122.4082) → geohash "9q8yz"  ← same prefix = nearby

Redis supports geospatial indexing natively:

# Driver goes online — add to Redis geo index
GEOADD drivers:active -122.4194 37.7749 "driver_123"

# Driver sends location update every 3 seconds
GEOADD drivers:active -122.4200 37.7750 "driver_123"  ← overwrites in-place

# Find drivers within 5 km of rider
GEORADIUS drivers:active -122.4194 37.7749 5 km
           WITHDIST WITHCOORD COUNT 10 ASC

Result: O(log n) query, sub-millisecond response, ~33K writes/sec easily handled by Redis Cluster.

Limitation: Geohash cells are rectangular — boundary cases may miss drivers just outside the cell. Fix: always search the 8 neighboring cells as well.

Approach 4 — H3 Hexagonal Index (What Uber Actually Uses)

Uber open-sourced H3 — a hexagonal hierarchical geospatial indexing system that resolves the rectangular boundary problem of geohashing.

flowchart LR
    DRIVER["Driver GPS Location"]

    H3["H3 Indexing Engine"]

    CELL["Hexagonal Grid Cell"]

    DB["Geo Store (Redis / Cassandra)"]

    RIDES["Ride Request"]

    SEARCH["Radius / gridDisk Query"]

    MATCH["Nearby Drivers Result"]

    DRIVER --> H3 --> CELL --> DB

    RIDES --> H3 --> SEARCH --> DB --> MATCH

Why hexagons?

  • All 6 neighbors of a hexagon are equidistant from the center — rectangles are not
  • No edge distortion — hexagons cover space more uniformly at all latitudes
  • Hierarchical — resolution 9 cells are ~0.1 km²; resolution 7 cells are ~5 km²
Find nearby drivers:
  1. Convert rider location to H3 hex ID (resolution 9)
  2. Get the hex + 1 ring of surrounding hexes (7 hexes total)
  3. Query Redis for all drivers with a hex ID in that set
  4. Sort results by actual distance (Haversine on the small candidate set)

Geospatial Approach Comparison

Approach Latency Write Throughput Accuracy Used At
Naive SQL High Low Exact Prototypes
PostGIS Medium Medium Exact Small-medium scale
Geohashing + Redis Very low Very high Good (boundary edge cases) Most ride-hailing apps
H3 + Redis Very low Very high Excellent Uber production

Step 6 — Real-Time Location Tracking

Driver → Server (Location Updates)

sequenceDiagram
    participant DriverApp
    participant LS as Location Service
    participant REDIS as Redis Geo Index
    participant WS as WebSocket Server
    participant RiderApp

    loop Every 3 seconds
        DriverApp->>LS: POST /location {driver_id, lat, lon, heading}
        LS->>REDIS: GEOADD drivers:active lon lat driver_id
        LS->>WS: Publish location update for active ride
        WS-->>RiderApp: Push {driver_id, lat, lon, eta_sec}
    end

Server → Rider (Real-Time Tracking)

Two options for delivering driver location to the rider app:

Method How It Works Latency Server Load Recommendation
Polling Rider app calls GET /location/{driver_id} every 3s ~3s High (many HTTP connections) Fallback only
WebSocket Persistent connection; server pushes updates < 500ms Low (one connection per active ride) Primary
Server-Sent Events One-way push from server ~1s Medium Alternative to WebSocket

WebSocket flow during an active ride:

1. Rider app opens WebSocket to ws://tracking.uber.com/rides/{ride_id}
2. Server subscribes this connection to location updates for that ride
3. Every time Location Service receives an update → push to subscribed WebSocket
4. Rider sees driver moving on map in near real-time
5. When ride completes → server closes WebSocket connection

Location Service Internals

flowchart LR
    DRIVER[Driver App\n33K updates/sec] --> LB[Load Balancer]
    LB --> LS1[Location Service\nInstance 1]
    LB --> LS2[Location Service\nInstance 2]
    LB --> LS3[Location Service\nInstance 3]
    LS1 & LS2 & LS3 --> REDIS[Redis Cluster\nGeo Index\n~5 MB hot data]
    LS1 & LS2 & LS3 --> KF[Kafka\nlocation.updated]
    KF --> HIST[(Cassandra\nLocation History)]

    style REDIS fill:#fff4e0,stroke:#f59e0b
    style KF fill:#ede9fe,stroke:#7c3aed
  • Redis holds only the latest known position for each driver — small (~5 MB for 100K drivers)
  • Kafka streams all location events for analytics, ETA recalculation, and route reconstruction
  • Cassandra stores location history per ride (used for fare calculation and dispute resolution)

Step 7 — ETA Computation

ETA is calculated at two moments:

  1. Before booking — ETA for the nearest driver to reach the rider (pickup ETA)
  2. During the ride — Estimated time from current position to destination (drop-off ETA)

Pickup ETA Flow

flowchart LR
    RideReq[Ride Request\npickup: A] --> MATCH[Matching Service]
    MATCH --> LS[Location Service\nGet 10 nearest drivers]
    LS --> ROUTE[Routing Service]
    ROUTE -->|driver_1 ETA: 3 min| MATCH
    ROUTE -->|driver_2 ETA: 5 min| MATCH
    ROUTE -->|driver_3 ETA: 7 min| MATCH
    MATCH --> SELECT[Select driver_1\nlowest ETA + rating]

ETA Calculation Inputs

Input Source
Driver current position Redis Geo Index
Pickup location Rider request
Road network graph Google Maps / Mapbox API or internal map service
Real-time traffic Traffic layer from map provider
Historical trip data Cassandra — actual vs predicted times on this route

ETA Formula

ETA (seconds) = Routing Service computation:
  = distance_on_road (meters) / average_speed_on_route (m/s)
  × traffic_congestion_factor (1.0 = free flow, 2.0 = heavy traffic)
  + stop_delay_factor (traffic lights, intersections)

The Routing Service calls Google Maps Distance Matrix API or a self-hosted routing engine (OSRM / Valhalla) for this computation. Uber uses its own internal map engine at scale.


Step 8 — Surge Pricing

Surge pricing is Uber's mechanism for balancing supply and demand by raising fares when demand exceeds supply in a geographic area.

How Surge Is Computed

flowchart TD
    DemandSignal[Ride Requests\nlast 5 min\nin each H3 cell] --> SURGE[Surge Engine]
    SupplySignal[Available Drivers\nper H3 cell] --> SURGE
    SURGE --> RATIO{demand / supply ratio}
    RATIO -->|< 1.2| M1[Multiplier: 1.0x\nNormal pricing]
    RATIO -->|1.2 – 1.5| M2[Multiplier: 1.5x\nSlight surge]
    RATIO -->|1.5 – 2.0| M3[Multiplier: 2.0x\nModerate surge]
    RATIO -->|> 2.0| M4[Multiplier: 2.5x – 3x\nHigh surge]

Surge Pricing Formula

Total Fare = (Base Fare + (Cost/km × distance) + (Cost/min × time)) × Surge Multiplier

Example (San Francisco, 5 km ride, 12 minutes):
  Base fare:         $2.00
  Distance charge:   $1.50/km × 5 km    = $7.50
  Time charge:       $0.30/min × 12 min = $3.60
  Subtotal:          $13.10
  Surge multiplier:  1.5x
  Total fare:        $13.10 × 1.5       = $19.65

Surge multipliers are recomputed every minute per H3 cell and cached in Redis with a 90-second TTL.


Step 9 — Database Design

Database Selection

Data Database Reason
Rider + driver profiles PostgreSQL ACID — payment methods and auth must be strongly consistent
Ride records + history PostgreSQL Transactional — fare, status changes, dispute resolution
Payments PostgreSQL Financial data requires ACID guarantees
Ratings PostgreSQL Append-only; low volume; needs joins to rides
Driver current location Redis Sub-ms geo queries; small data; volatile
Location history per ride Cassandra Append-only time-series; high write volume
Surge cache Redis Per-cell multiplier; 90s TTL; read-heavy
Active ride state Redis Fast reads for WebSocket server; short TTL

Rides Table (PostgreSQL)

Table: rides

  ride_id          BIGINT         PRIMARY KEY
  rider_id         BIGINT         FK → users
  driver_id        BIGINT         FK → drivers (NULL until assigned)
  status           VARCHAR        REQUESTED | DRIVER_ASSIGNED | IN_PROGRESS | COMPLETED | CANCELLED
  pickup_lat       DECIMAL(9,6)
  pickup_lon       DECIMAL(9,6)
  dropoff_lat      DECIMAL(9,6)
  dropoff_lon      DECIMAL(9,6)
  fare_estimate    DECIMAL(10,2)  (shown before booking)
  fare_final       DECIMAL(10,2)  (set at completion)
  surge_multiplier DECIMAL(4,2)   DEFAULT 1.0
  distance_km      DECIMAL(8,3)
  duration_sec     INT
  requested_at     TIMESTAMP
  assigned_at      TIMESTAMP
  started_at       TIMESTAMP
  completed_at     TIMESTAMP
  cancelled_at     TIMESTAMP
  cancel_reason    VARCHAR

Drivers Table (PostgreSQL)

Table: drivers

  driver_id        BIGINT         PRIMARY KEY
  user_id          BIGINT         FK → users
  license_plate    VARCHAR(20)    NOT NULL
  vehicle_make     VARCHAR(50)
  vehicle_model    VARCHAR(50)
  vehicle_year     INT
  is_online        BOOLEAN        DEFAULT false
  avg_rating       DECIMAL(3,2)   DEFAULT 5.0
  total_rides      INT            DEFAULT 0
  created_at       TIMESTAMP

Location History (Cassandra)

Table: driver_location_history

Partition key:  ride_id        BIGINT
Clustering key: recorded_at    TIMESTAMP DESC

Columns:
  driver_id   BIGINT
  lat         DECIMAL(9,6)
  lon         DECIMAL(9,6)
  heading     INT            (0–359 degrees)
  speed_kmh   DECIMAL(5,2)

PRIMARY KEY (ride_id, recorded_at)

Used for:

  • Route replay (rider can see their trip path after completion)
  • Fare dispute resolution (actual distance traveled)
  • Driver behavior analytics

Ratings Table (PostgreSQL)

Table: ratings

  rating_id      BIGINT         PRIMARY KEY
  ride_id        BIGINT         FK → rides (UNIQUE — one rating per ride per rater)
  rated_by       BIGINT         FK → users
  rated_for      BIGINT         FK → users
  score          SMALLINT       (1–5)
  comment        TEXT
  created_at     TIMESTAMP

Step 10 — Caching Strategy

Redis Cache Map

Cache Key TTL Contents
Driver live location drivers:active (Geo set) None Overwritten every 3 sec by location updates
Active ride state ride:{ride_id} 2 hrs Status, driver_id, pickup coords
Driver online status driver:online:{driver_id} 5 min 1 = online; refreshed on each update
Surge multiplier surge:{h3_cell_id} 90 sec Float multiplier per geographic cell
Fare estimate cache fare:{pickup_h3}:{drop_h3} 5 min Cached estimate for common route pairs
User session / JWT session:{user_id} 30 days Auth token

Active Ride State in Redis

When a ride is IN_PROGRESS, the WebSocket server needs to push updates with sub-second latency. Reading from PostgreSQL on every location update would be too slow.

{
  "ride_id":    "91234",
  "rider_id":   "u_5678",
  "driver_id":  "d_9012",
  "status":     "IN_PROGRESS",
  "pickup_lat":  37.7749,
  "pickup_lon": -122.4194,
  "drop_lat":    37.7853,
  "drop_lon":   -122.4089
}

This Redis entry is the source of truth for real-time operations. PostgreSQL is updated asynchronously via Kafka.


Step 11 — Payment Flow

sequenceDiagram
    participant RS as Ride Service
    participant KF as Kafka
    participant PAY as Payment Service
    participant STRIPE as Stripe / PayPal
    participant NOTIF as Notification Service

    RS->>RS: UPDATE status = COMPLETED
    RS->>RS: Calculate final fare (distance × rate × surge)
    RS->>KF: Publish ride.completed {ride_id, fare, rider_id}

    KF->>PAY: Consume event
    PAY->>PAY: Lookup rider's default payment method
    PAY->>STRIPE: Charge {amount, card_token}
    STRIPE-->>PAY: {success, transaction_id}
    PAY->>PAY: INSERT INTO payments
    PAY->>KF: Publish payment.success {ride_id, transaction_id}

    KF->>NOTIF: Consume payment.success
    NOTIF->>Rider: Push "Ride receipt: $19.65"
    NOTIF->>Driver: Push "Payment received: $14.70"

Payment is asynchronous — the ride completion does not block waiting for the charge to clear. If the payment fails, a retry queue attempts up to 3 times before flagging the account.


Step 12 — Scaling

Location Service Scaling

flowchart LR
    DRIVERS[100K Drivers\n33K updates/sec] --> LB[Load Balancer\nConsistent Hash by driver_id]
    LB --> LS1[Location Service 1]
    LB --> LS2[Location Service 2]
    LB --> LS3[Location Service 3]
    LS1 & LS2 & LS3 --> RC[Redis Cluster\n10 shards]
  • Consistent hashing by driver_id ensures each driver always hits the same Location Service pod — avoids race conditions on Redis GEOADD
  • Redis Cluster with 10 shards handles 100K+ writes/second easily
  • Location data is hot and small — 100K drivers × 50 bytes = ~5 MB total, fits in a single Redis node's memory

Ride Service Scaling

  • Ride Service is stateless — scale horizontally
  • PostgreSQL rides table uses range partitioning by created_at — current month's rides stay in a hot partition; older data is archived to cold storage
  • Read replicas absorb ride history queries (receipts, analytics)

Matching Service Scaling

For each incoming ride request:
  1. Query Redis GEORADIUS → get 10 nearest drivers (< 1ms)
  2. Rank by ETA using Routing Service cache (most ETAs are pre-cached)
  3. Send push notification to top driver
  4. All steps complete in < 500ms total

The Matching Service is CPU-bound for ranking — scale horizontally, no shared state.

Database Sharding

Database Shard Key Reason
Rides rider_id % N Rider's ride history stays on one shard
Payments rider_id % N Co-located with rides for fast joins
Location history ride_id % N All location points for one ride on one shard

Step 13 — Failure Scenarios

Failure Impact Mitigation
Redis (geo index) down Cannot find nearby drivers Redis Sentinel failover < 30s; fallback to PostGIS query on PostgreSQL
Matching Service down No new rides matched Auto-restart via Kubernetes; Kafka queues ride requests during downtime
Driver app loses connectivity Location updates stop Location TTL in Redis = 60s; driver marked offline after 60s of silence
Payment Service down Charges not processed Ride completes normally; payment event stays in Kafka; processed when service recovers
WebSocket server crashes Rider tracking disconnects Rider app falls back to polling; reconnects to new WebSocket pod
Ride Service crash mid-ride Ride state lost Ride state persisted in both Redis and PostgreSQL; recovered on restart
Kafka consumer lag Events processed late Kafka RF=3; consumer group rebalances; critical events (payment) processed first via priority topic
PostgreSQL primary down Ride writes fail Automatic failover to standby replica in < 60 seconds

Final Architecture

flowchart TD
    RIDER[Rider App] --> AG[API Gateway]
    DRIVER[Driver App] --> AG

    AG --> US[User Service]
    AG --> DS[Driver Service]
    AG --> RS[Ride Service]
    AG --> LS[Location Service]

    RS --> MATCH[Matching Service]
    RS --> ROUTE[Routing Service\nGoogle Maps]
    RS --> PRICE[Pricing Service]
    RS --> KF[Kafka Cluster]

    LS --> REDIS[Redis Cluster\nGeo Index + Surge Cache]
    MATCH --> REDIS

    KF --> PAY[Payment Service]
    KF --> RATE[Rating Service]
    KF --> NOTIF[Notification Service]
    KF --> HIST[Location History\nConsumer]

    RS --> PG[(PostgreSQL\nRides + Payments + Users)]
    HIST --> CASS[(Cassandra\nLocation History)]

    RS --> WS[WebSocket Server]
    WS --> RIDER

Technology Stack

Layer Technology
API Gateway NGINX / Envoy + JWT auth
Ride Service Java / Go (stateless microservices)
Location Service Go (high-throughput, low GC overhead)
Matching Service Python / Go
Routing Service Google Maps API / OSRM (self-hosted)
Pricing / Surge Engine Python + Redis
Message Queue Apache Kafka (RF=3, 10 partitions per topic)
Driver location cache Redis Cluster (GEOADD / GEORADIUS)
Geo indexing H3 Hexagonal Index (Uber open source)
Ride + user database PostgreSQL (primary + read replicas)
Location history Apache Cassandra
Real-time tracking WebSocket (Socket.IO / native WS)
Push notifications Firebase Cloud Messaging / APNs
Payment processing Stripe / PayPal gateway
Monitoring Prometheus + Grafana + Jaeger (distributed tracing)
Deployment Kubernetes multi-region (AWS + GCP)

Key Trade-Offs

Decision Option A Option B Choice and Reason
Driver location store PostgreSQL + spatial index Redis GEOADD Redis — 33K writes/sec and sub-ms geo queries; PostgreSQL cannot keep up
Geospatial indexing Geohashing (rectangles) H3 hexagonal index H3 — uniform coverage, better boundary accuracy, equidistant neighbors
Rider tracking delivery REST polling WebSocket push WebSocket — < 500ms latency; polling would create 5K extra HTTP req/sec
Ride state consistency Cassandra (eventual) PostgreSQL (strong) PostgreSQL — rides involve money; strong consistency is required
Location history PostgreSQL Cassandra Cassandra — append-only time-series; 33K writes/sec fits Cassandra perfectly
Payment timing Synchronous (block on charge) Async via Kafka Async — ride completion must be fast; payment retry logic handles failures
Surge pricing update interval Real-time per request Batch every 60–90 seconds Batch — real-time would overload pricing engine; 90s staleness is acceptable

Common Interview Mistakes

  • ❌ Using PostgreSQL for live driver locations — 33K writes/sec with geo queries requires Redis
  • ❌ Not designing a state machine for the ride — a ride has 6+ states; missing any = money or safety bug
  • ❌ Polling for rider tracking instead of WebSockets — polling at scale creates enormous unnecessary load
  • ❌ Not explaining geohashing or H3 — the nearby-driver query is the hardest problem; examiners expect detail
  • ❌ Ignoring driver decline cascade — if a driver declines, the system must re-match; explain the fallback
  • ❌ Making payment synchronous — blocking ride completion on Stripe response creates latency spikes
  • ❌ No surge pricing design — Uber's pricing model is a common deep-dive question
  • ❌ No explanation of ETA recalculation — ETAs change during the ride; the system must handle that
  • ❌ Forgetting driver offline detection — if GPS updates stop, the driver must be removed from the geo index
  • ❌ No location history — dispute resolution and route replay require stored location history

Interview Questions

  1. How does Uber find the nearest available driver for a ride request in milliseconds?
  2. What is geohashing and what are its limitations for proximity search?
  3. Why did Uber switch from geohashing to H3 hexagonal indexing?
  4. How do you handle 33,000 driver location writes per second?
  5. How does real-time driver tracking work on the rider's app? Why WebSockets instead of polling?
  6. Walk me through the complete ride booking flow from tap to driver assignment.
  7. What happens if the first driver declines the ride? How does the matching cascade work?
  8. How does surge pricing work? How do you compute and cache surge multipliers?
  9. Why is the ride state machine important? What states does a ride go through?
  10. How do you ensure payment is processed even if the Payment Service crashes mid-flow?
  11. How would you handle a city-wide event (concert, New Year's Eve) with 10x normal demand?
  12. How do you detect that a driver has gone offline if their app loses connectivity?
  13. What database would you use for ride history and why not Cassandra?
  14. How do you prevent the same driver from being dispatched to two rides simultaneously?
  15. How would you design the ETA recalculation system during an active ride?

Summary

Concern Solution
Nearby driver search Redis GEORADIUS + H3 hex index — sub-ms, 33K writes/sec
Driver location updates Location Service → Redis GEOADD (overwrite latest position) + Kafka for history
Rider real-time tracking WebSocket push from Location Service; fallback to polling
Ride state PostgreSQL state machine — REQUESTED → ASSIGNED → IN_PROGRESS → COMPLETED
ETA computation Routing Service (Google Maps / OSRM) + real-time traffic layer
Surge pricing demand/supply ratio per H3 cell → multiplier cached in Redis (90s TTL)
Payment Async via Kafka ride.completed event → Payment Service → Stripe
Location history Cassandra time-series per ride (dispute resolution, route replay)
Driver decline cascade Matching Service tries up to 5 drivers; expands radius if all decline
Scale Stateless services + Redis Cluster + Kafka + PostgreSQL read replicas

The core principle: Uber is a geospatial matching problem at massive concurrency. The key insight is keeping driver locations in Redis (not PostgreSQL) for sub-millisecond geo queries, using H3 hexagonal indexing for accurate proximity search, and WebSockets for real-time tracking. Ride state must be in PostgreSQL because it involves financial transactions.