Design Uber
Design Uber at 15M trips/day, 5M drivers, 1M+ concurrent trips. Covers Redis GEO sorted sets for nearest-driver queries, H3 hex cells for surge zones, 1-3s driver location updates, parallel dispatch to top-3 drivers with atomic Lua-script concurrency control, surge pricing recomputed every 5 min from demand/supply streams, GPS map-matching with Kalman filtering, and per-city geospatial sharding.
Foundational.
How it works
What are we designing? Uber — a ride-hailing platform that connects riders with drivers in real time. A rider requests a ride; the system finds nearby drivers, dispatches one, tracks the ride in progress, processes payment, and adjusts price dynamically based on supply and demand (surge pricing).
The defining challenges are geospatial: how do you find the nearest available drivers to a rider in <1 second, track 1M+ driver locations in real time, and balance supply and demand at city-block granularity? Most other systems we design are key-value or graph-shaped; Uber is fundamentally a spatial system.
Functional requirements.
- Rider requests a ride; system matches them with a nearby driver.
- Driver app reports location every 1-3 seconds.
- Rider app shows driver location on the map in real time during ride.
- System computes ETA, price, and route.
- Surge pricing: price multipliers by zone based on demand/supply.
- Trip history and receipts.
- Rating system after each ride.
Non-functional requirements.
- Dispatch latency: < 1 s from ride request to driver notification.
- Driver location update: every 1-3 s; p99 delivery to rider < 2 s.
- Availability: 99.99% (an outage leaves riders stranded).
- Consistency: a driver can only be assigned to one ride at a time.
- Scale: 15M trips/day, 5M drivers, 100M+ riders, 1M+ concurrent trips.
Non-goals. No delivery (Uber Eats), no freight, no autonomous vehicles.
Capacity estimation.
Trips. 15M trips/day. Average trip ~30 min, so concurrent trips at peak ~15M x 30/(24x60) = ~312K concurrent trips.
Driver location updates. 5M drivers online at peak, each sending a location update every 3s = 1.67M updates/sec. Each update is ~100 bytes (driver_id, lat, lng, ts). 1.67M x 100 B = 167 MB/s = 1.3 Gbps of location ingress.
Rider location updates. ~312K active riders sending every 5s = 62K updates/sec, smaller volume but similar pattern.
Dispatches. 15M trips/day / 86400s = ~175 dispatches/sec average, peak ~500/sec. Each dispatch queries nearby drivers (radius ~3km) and notifies up to 3 drivers in parallel.
Storage. Trip records: 15M/day x ~1KB = 15 GB/day = ~5.5 TB/year. Driver location history: 1.67M updates/sec x 100 B x 86400s = 14 TB/day — too much to keep hot; sample to every 30s for archival, keep only last 30 days for live queries.
Surge updates. Surge multipliers per zone, recomputed every 5 min. ~10K zones globally. Surge is small data but read-heavy.
APIs.
POST /v1/rides (rider_id, pickup, dropoff, ride_type)
-> { ride_id, drivers_notified, eta, surge_multiplier, price }
PATCH /v1/rides/:id (driver accepts, completes, cancels)
GET /v1/rides/:id (status, driver, eta)
POST /v1/drivers/:id/location (lat, lng, heading, speed) -- every 1-3s
GET /v1/rides/:id/location (current driver location for rider map)
GET /v1/surge?lat=..&lng=.. (current surge multiplier at a point)Driver location is the hot path: drivers push every few seconds via WebSocket or HTTP POST. Rider location during a ride is the read counterpart.
Data model.
Driver location (Redis GEORADIUS, hot): each driver's current location is stored in a Redis GEO set per city:
key: drivers:{city_id}
type: GEO sorted set
members: driver_id, score = geohash of (lat, lng)GEORADIUS drivers:nyc 40.74 -73.99 3 km → returns drivers within 3km.
Rides (sharded SQL, source of truth):
rides (id BIGINT PK, rider_id, driver_id NULL, pickup GEO,
dropoff GEO, status ENUM('requested','accepted','en_route','completed','canceled'),
surge_mult FLOAT, price_cents INT, created_at, completed_at NULL)Drivers (sharded SQL):
drivers (id, name, car_model, license_plate, rating_avg, status ENUM('online','offline','busy'),
current_lat, current_lng, current_zone_id)
INDEX (status, current_zone_id)Surge zones (Redis, computed every 5 min):
key: surge:{zone_id}
value: { multiplier: 1.5, updated_at: ts }
TTL: 10 minTrip history (Cassandra, partitioned by rider_id):
trip_history (rider_id, ride_id, ts, driver_id, price_cents, ...)
PRIMARY KEY ((rider_id), ts)Time-ordered per rider, RF=3.
Deep dive: geospatial indexing (geohash vs quadtree) and dispatch.
The hardest problem is: 'given a rider at (lat, lng), find the nearest available drivers within 3 km, in <1 second, across 1M+ drivers.' Three approaches:
1. Redis GEO (geohash). Redis stores driver locations in a sorted set scored by geohash (a 52-bit encoding of lat/lng into a single integer that preserves proximity). GEORADIUS is O(log N + M) where M is the number of matches. This is what most ride-hailing systems use as the first cut. Pros: simple, fast, built-in. Cons: limited filtering (you can't filter by 'driver has 4+ rating' in Redis; must fetch and filter in app code).
2. Quadtree. Recursively subdivide the map into 4 quadrants until each leaf has <K drivers. To find nearby drivers, walk the tree down to the rider's leaf, then expand to neighbors until enough drivers are found. Pros: more flexible (can store metadata per node, e.g. only drivers with rating >= 4.5). Cons: harder to update (drivers move constantly, so the tree is rebuilt frequently).
3. Google S2 / H3 cells. Hierarchical hexagonal (H3, from Uber itself) or spherical (S2, from Google) cell systems. Each cell has a 64-bit ID; nearby cells have nearby IDs. Hexagonal cells avoid the 'diagonal neighbor' problem of quadtrees (every hex has 6 equidistant neighbors). H3 is what Uber actually uses internally for surge zones and driver search.
Our choice: Redis GEO + H3 zones. Redis GEO handles the fast 'nearest drivers' query; H3 cells drive surge pricing and zone-based dispatch (e.g. only dispatch to drivers in the rider's H3 cell or its 6 neighbors).
Dispatch flow. When a ride is requested:
- The Dispatch Service runs GEORADIUS drivers:nyc lat lng 3 km, getting ~10-50 candidate driver_ids.
- Fetch driver metadata from Redis (status, rating, vehicle type) — a batched MGET against driver_meta:{id} keys.
- Filter: status=online, rating >= 4.5, vehicle matches ride_type.
- Rank by distance + rating (closer and higher-rated first).
- Send a 'ride offer' WebSocket/push to the top 3 drivers in parallel.
- First driver to accept gets the ride; others get a 'ride taken' message.
- If no driver accepts in 10s, expand the radius to 5km and retry.
Driver concurrency control. A driver can only be on one ride at a time. When a driver accepts, we SET their status to 'busy' atomically via a Lua script (so two concurrent ride offers can't both be accepted). The losing offers are rolled back to 'online' status.
Surge pricing. A background worker aggregates ride requests and online drivers per H3 zone every 5 min, computes a demand/supply ratio, and emits a multiplier (e.g. 1.0x normal, 1.5x high demand, 2.5x very high). The multiplier is published to Redis and read by the Ride Service when pricing a new ride. Surge updates push notifications to drivers in high-surge zones (incentivizing them to drive there).
Bottlenecks and failure modes.
-
Redis GEO hot shard. All drivers in NYC live in one Redis GEO key; a single shard handles all NYC queries. Mitigation: shard by zone (one key per borough); replicate read-heavy keys.
-
Driver location write hotspot. 1.67M driver location updates/sec, each updating a Redis GEO key. A single Redis primary handles ~100K writes/sec. Mitigation: shard Redis by city; use Redis Cluster with 16+ shards per metro.
-
Dispatch thundering herd. A popular event (concert ending) triggers 10K ride requests in 1 minute, all from the same area. Mitigation: queue requests; rate-limit dispatch; pre-position drivers (predict where demand will be).
-
Driver accept race. Two rides try to dispatch the same driver simultaneously. Mitigation: Lua script atomic SET status='busy' IF status='online'; only one wins.
-
GPS jitter. Driver GPS bounces around when in a tunnel; the rider map shows the car teleporting. Mitigation: snap GPS to the road network (map matching); smooth the location stream with a Kalman filter.
-
WebSocket failure during ride. If the rider's WebSocket drops, they can't see driver location. Mitigation: client retries with backoff; falls back to HTTP polling every 5s; driver-side ETA still works.
-
Surge lag. Surge is recomputed every 5 min; a sudden demand spike takes 5 min to reflect in pricing. Mitigation: trigger an immediate surge recompute when demand in a zone exceeds 2x normal.
-
Payment failure mid-trip. The rider's card is declined at trip end. Mitigation: charge a pre-authorization hold at ride request; if the hold fails, reject the ride request before dispatch.
Scaling strategy and trade-offs.
Geospatial sharding. Shard by city — each metro has its own Redis GEO cluster, Dispatch Service instances, and trip storage. Cross-city traffic is zero (you can't drive from NYC to LA mid-ride).
Driver location write path. Drivers POST location every 1-3s; the Location Service batches writes per driver (one Redis update per second, not per request) to reduce Redis load. Driver apps throttle themselves on slow networks.
Multi-region. Each city runs in its nearest region. Cross-region replication is not needed for live data (drivers and rides are local); only trip history is replicated to a central warehouse for analytics.
Surge scaling. Per-zone multipliers are small data; the surge worker can run in a single region per metro, writing to a local Redis that the Ride Service reads from.
Trade-offs made explicit.
- We chose Redis GEO over a custom quadtree — gained operational simplicity and built-in commands, lost rich filtering (must fetch and filter).
- We chose H3 hex cells for zones — gained equidistant neighbors (vs quadtrees' diagonal problem), lost the S2 ecosystem's maturity.
- We chose 1-3s driver location update — gained fine-grained rider map, lost battery on the driver's phone (mitigated by adaptive update rate).
- We chose per-city sharding — gained isolation, lost cross-city optimization (e.g. a driver near a city boundary could pick up rides in either).
- We chose 5-min surge recompute — gained low surge compute cost, lost real-time surge accuracy (mitigated by event-triggered recomputes on spikes).
A rider in NYC requests a ride. The system needs to find nearby drivers. Which data structure should the Dispatch Service query?
Pick one answer.
Two rides simultaneously dispatch the same driver. Without protection, both could 'accept' the driver. How do you prevent this?
Pick one answer.
Engineering mental model
Mental model. Think of Design Uber as a deliberate boundary in a system. The boundary exists because something becomes harder to manage when everything is done in one place: latency, scale, failure isolation, consistency, cost, or team ownership. The useful question is not “what does Design Uber mean?” but “what pressure makes this boundary worth introducing, and what new failure mode does it create?”
Before choosing Design Uber, name the workload, the critical user path, the dominant bottleneck, the failure you are trying to absorb, and the trade-off you are willing to accept. If you cannot name those five things, the design is probably premature.
// Pseudocode
request = receive()
result = design_uber(request)
return result
// Production questions:
// 1. What happens on timeout?
// 2. Can this operation be retried safely?
// 3. What is the bottleneck?Back-of-the-envelope reasoning
Numerical lens: write down traffic, payload size, read/write ratio, peak multiplier and durability target before choosing a component. The numbers should justify the architecture.
Interactive thought experiment: Design Uber
Change the variables below and predict what breaks first in Design Uber. The production lab can later reuse these same inputs.
Change one variable at a time. Predict the failure mode first, then move the slider and see whether your mental model matches the simplified system response.
If you are stuck on Design Uber, start by drawing the request path and marking every network hop, stateful component, queue, cache and failure boundary. Then estimate where the system will saturate.
You increase traffic by 10× in a system using Design Uber. What should you inspect first?
Pick one answer.
Which statement is the safest engineering habit when using Design Uber?
Pick one answer.
You have dashboards for traffic, latency, errors and saturation. You can change the architecture, but every change has operational cost.
Production scenario: your system uses Design Uber, traffic suddenly spikes, and p99 latency doubles. What is your first move?
Interview drill
Answer this without notes: When would you choose Design Uber, and when would you intentionally avoid it? Mention at least one bottleneck it addresses, one failure mode it introduces, and one alternative. Then quantify the workload you are designing for.
A useful engineering lens for Design Uber: define the problem it solves, the simpler design that fails first, the constraint that forces you to introduce this concept, and the new failure modes the concept creates.
Numerical sanity check
Back-of-the-envelope reasoning beats fake precision. State your traffic, payload, concurrency and growth assumptions explicitly, then calculate enough to know whether the current architecture is orders of magnitude away from the target.
Do not optimize for a memorized definition. Reason from the workload and failure mode.
Imagine the simplest version of a system using Design Uber. What breaks first as traffic grows by 10×, and what would you change before reaching 100×?
Pick one answer.
What you gain, what you pay
- +Redis GEORADIUS gives O(log N + M) nearest-driver queries in <10 ms.
- +Atomic Lua-script driver assignment prevents double-booking races.
- +H3 hex cells make surge zones equidistant (no quadtree diagonal problem).
- +Per-city sharding isolates traffic and bounds Redis GEO key size.
- −Redis GEO hotspot for large metros (NYC has 500K+ drivers in one key) — needs zone sharding.
- −Driver location write load (1.67M updates/sec) is heavy — needs batching + sharding.
- −Surge recompute every 5 min lags sudden demand spikes — needs event-triggered recomputes.
- −GPS jitter shows drivers teleporting on the rider map — needs map matching + Kalman filter.
How this breaks in production
- Redis GEO hot shard in big metros — needs zone-level sharding.
- Driver accept race — needs atomic Lua check-and-set.
- Dispatch thundering herd at event endings — needs queueing + pre-positioning.
- GPS jitter on the rider map — needs map matching + Kalman filter.
- Payment failure mid-trip — needs pre-auth at ride request.
Don't fall into these traps
- •Full table scan for nearby drivers — O(N) is far too slow at 1M drivers.
- •Driver location updates going straight to SQL — too slow and write-heavy.
- •Single global Redis for all drivers — hotspot and single point of failure.
- •Non-atomic driver accept (SET status='busy' without checking) — double-booking.
- •Surge recompute on every ride request — too expensive and inconsistent.
Real systems using this
How real systems implement this
- Uber — H3 hex cells for zones, Redis-style geo index for nearest drivers, Kafka for event streaming, geobased sharding per city. Documented in Uber engineering blog 'H3: Hexagonal Hierarchical Spatial Index'.
- Lyft — Similar architecture; uses a custom quadtree and S2 cells. Per-city sharding. Documented in their engineering blog.
- DoorDash — Adapts the Uber pattern for food delivery: rider becomes 'customer', driver becomes 'dasher', ETA computation is more complex (restaurant prep time + drive time).
Practice saying it out loud
- Q1Design Uber. How do you find the nearest driver to a rider?
- Q2Two rides try to dispatch the same driver. How do you prevent double-booking?
- Q3How do you implement surge pricing?
- Q4A driver's GPS bounces around in a tunnel. How do you fix the rider map?
Further reading & references
Core explanations are original NO CAP material. External references are provided for deeper study and standards.
What next?
Mark as understood once the mental model clicks.
Next recommended
Design Ride Matching