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