Uber
Walk through a complete Uber design, from a single trip service to a globally distributed system handling 5M concurrent drivers, real-time GPS matching, and sub-5-second dispatch.
What is Uber?
Uber is a ride-hailing platform that connects riders who need a trip with nearby drivers. The apparent core is simple: request a ride, match a driver, complete the trip. The hard part is underneath.
The system must continuously track millions of driver GPS coordinates, answer "who is closest to this pickup?" in under 100ms, stream location updates between two strangers in near real time, and do this for hundreds of thousands of concurrent trips without losing durable trip state. The design separates the location write path (drivers broadcasting GPS) from the matching read path (riders triggering geospatial queries), because they have different traffic shapes and failure modes.
It tests geospatial indexing, real-time streaming, event-driven matching, and atomic concurrency control.
TL;DR
Separate three planes: high-volume driver-location ingestion, asynchronous trip matching, and WebSocket location streaming. Write available-driver positions to a regionally partitioned Redis Geo index, publish trip requests to Kafka, and let Match Workers offer a small candidate set in parallel.
Use a conditional database update plus a driver-claim guard so only one driver wins a trip and one driver cannot be assigned twice. For active trips, publish GPS updates to a per-trip channel and push them to the rider over WebSocket. Compute surge asynchronously per geographic cell and read the current multiplier from Redis on the request path.
Scope and assumptions
The following are illustrative interview planning assumptions, not a specification of the named product or a guarantee from any vendor:
- Up to 1 million concurrently active drivers, each sending a location update about every 4 seconds: roughly 250,000 location writes/second at the stated peak.
- Approximately 5 million registered drivers, 15 million trips/day, and up to 500 trip requests/second during a surge. Up to 500,000 active trips may hold rider location streams.
- A match prioritizes nearby available drivers within an initial 5 km radius and offers the top few candidates concurrently. The first valid acceptance wins; "nearest" is a ranking preference, not a promise that the mathematically closest driver accepts.
- Driver location is ephemeral matching state. Trip status and assignment are durable. A few seconds of location staleness is acceptable, but a lost trip or double assignment is not.
- Online assignment is targeted within 5 seconds of a request; online location delivery is targeted within 5 seconds of a driver update. Cross-bank payments, fare settlement, ratings, and driver onboarding are separate systems.
- Redis Geo, Redis Pub/Sub, Kafka, and PostgreSQL are illustrative implementation choices. Their latency and throughput depend on topology, payload size, region, and capacity testing.
Functional Requirements
Core Requirements
- Riders can request a trip by specifying a pickup and dropoff location.
- Drivers continuously broadcast their GPS location to the system.
- The system matches a requesting rider to the nearest available driver.
- Riders and drivers can see each other's real-time location during an active trip.
Below the Line (out of scope)
- Payments and invoicing
- Ratings and reviews
- Scheduled rides and ride types (Pool, Comfort, Black)
- Driver onboarding and background checks
The hardest part in scope: geospatial matching. Every ride request triggers a query across millions of GPS coordinates to find the nearest available driver. The naive approach (SQL range query on lat/lng columns) collapses at scale. Efficient geospatial indexing is the central engineering problem this article solves.
Payments are below the line because the payment flow (charge, refund, driver payout) runs after trip completion and does not share the core matching or location-tracking path. An extension could publish a TripCompletedEvent to a dedicated payments topic. A Payments Service would consume it, calculate the fare using trip distance and any surge multiplier, and execute the charge asynchronously.
Ratings and reviews are below the line because they are a separate write-after-trip flow that does not affect the hot paths. An extension could store ratings in a Postgres table keyed by (trip_id, rater_id) and compute rolling rating averages in a background job rather than inline.
Scheduled rides require a separate scheduling layer. An extension could store scheduled trip requests in a persistent job queue and release them into the normal matching flow shortly before the scheduled pickup time, reusing the matching infrastructure.
Non-Functional Requirements
Core Requirements
- Availability: 99.99% uptime. A mildly stale driver location is acceptable for a short window; a lost trip or duplicate assignment is not.
- Match latency: Rider receives a driver assignment within 5 seconds of requesting a trip.
- Location freshness: Driver locations are current to within 5 seconds at all times.
- Location write throughput: Support 1M concurrently active drivers, each sending GPS updates every 4 seconds. That is 250K location writes per second at peak.
- Scale: 5M registered drivers, 15M trips per day. Peak matching throughput of approximately 500 trip requests per second during surge.
Below the Line
- Sub-second GPS update propagation to rider app during trip
- Surge pricing computation (important but not part of functional core)
Read/write ratio: Location writes (driver GPS broadcasts) are the dominant workload: 250K writes per second at peak. Trip requests (matching reads) are orders of magnitude lower: ~500 per second peak. But each trip request triggers a geospatial query across 1M+ driver positions. The write volume shapes the location storage architecture; the read access pattern shapes the geospatial index. They pull in different directions, and that tension drives every major design decision in this article.
The illustrative 5-second match target is comparable to the 4-second GPS update interval. A much longer delay suggests the matching path is stalled rather than merely waiting for fresh location data. The target rules out matching designs that require multiple sequential database round-trips with no cache or queueing strategy.
30-second answer / outline
- Ingest driver GPS updates through a stateless Location Service and keep only available drivers in a regionally partitioned Redis Geo index.
- Create a durable trip in PostgreSQL and publish a
TripRequestedEventthrough an outbox or equivalent reliable publication path. - Match asynchronously: query nearby drivers, reserve candidate offer slots briefly, and notify several candidates in parallel.
- Claim the trip with a conditional update and atomically claim the driver; reject losing acceptances and release their offers.
- Stream active-trip locations through Redis Pub/Sub to WebSocket nodes, and compute surge in a background worker with per-cell cached multipliers.
5-minute explanation
Start with the traffic split: the illustrative baseline has about 250K GPS writes per second but only about 500 trip requests per second. Location ingestion must therefore be cheap and ephemeral, while trip creation and assignment need durable state and stronger correctness.
The rider request creates a requested trip in PostgreSQL and emits an event. Match Workers consume it, query the local regional Redis Geo set, and offer a small nearest-candidate set concurrently. A driver acceptance uses a conditional trip update and a driver availability claim; this avoids relying on a long-lived distributed lock while preventing races on both sides of the assignment.
For an active trip, the driver continues sending location updates. The Location Service publishes them to a per-trip channel, and the Location Streaming Service forwards them over a rider WebSocket. A short replay buffer handles reconnection; the durable trip record remains the source of truth for lifecycle state.
Surge is not calculated inline. A background worker counts supply and demand by geographic cell, writes a short-lived multiplier to Redis, and lets the trip request read one value. The remaining design work is capacity, regional partitioning, event replay, privacy, rate limiting, and graceful behavior when Redis, Kafka, or a streaming node fails.
45-minute interview approach
This is a time-boxed plan for answering the design question in an interview, not a claim that the article should be read in 45 minutes.
- 0β5 minutes β Clarify the contract: Confirm rider and driver flows, candidate ranking, radius expansion, location freshness, match deadline, cancellation, regions, and whether fare or payment is in scope.
- 5β10 minutes β Establish scale: Use the illustrative active-driver, GPS-update, trip-request, active-trip, and stream-connection numbers. Separate location writes from trip-state writes and rider pushes.
- 10β15 minutes β Define APIs and invariants: Walk through trip creation, location updates, acceptance, status, and the rider WebSocket. State that a trip and a driver can each have only one active assignment.
- 15β22 minutes β Draw the location and trip paths: Show API Gateway, Location Service, Redis Geo, Trip Service, PostgreSQL, Kafka, and Match Workers. Mention an outbox for reliable trip-event publication.
- 22β30 minutes β Deep dive on matching: Compare SQL, geohash, and Redis Geo; then compare sequential and parallel offers. Explain driver reservation, conditional trip claim, retries, ghost-driver cleanup, and regional shards.
- 30β35 minutes β Deep dive on real-time delivery: Compare polling, SSE, and WebSocket. Cover Pub/Sub routing, sticky trip hashing, replay buffers, reconnects, and teardown on trip completion.
- 35β41 minutes β Reliability, security, and operations: Cover stale GPS, Kafka duplicates, Redis failure, notification failure, authentication, location privacy, rate limits, queue depth, and latency/freshness metrics.
- 41β45 minutes β Trade-offs and close: Compare precomputed versus inline surge, Redis Geo versus H3, and database versus Redis driver claims. Recap the three planes and invite follow-ups.
Core Entities
- Driver: A registered driver with a current
location(latitude, longitude),status(available, on_trip, offline), vehicle details, and adriver_id. Thestatusfield gates every matching query. - Rider: A registered user with a
rider_idwho can place trip requests. - Trip: A request-to-completion record with
trip_id,rider_id,driver_id,pickup_location,dropoff_location,status(requested, accepted, in_progress, completed, cancelled), andcreated_at. - DriverLocation: The current GPS snapshot for a driver:
driver_id,latitude,longitude,updated_at. Ephemeral; not a durable historical record.
The schema details (indexes, partition keys, TTLs) are deferred to the deep dives. These four entities are sufficient to drive the API design and High-Level Design.
API Design
Keep the API surface minimal: two actors (rider and driver) with four distinct action types.
Rider requests a trip:
POST /trips
Body: { pickup_lat, pickup_lng, dropoff_lat, dropoff_lng }
Response: { trip_id, status: "requested", estimated_wait_seconds }
Rider gets trip status and driver location:
GET /trips/{trip_id}
Response: { trip_id, status, driver: { lat, lng, eta_seconds } }
Driver broadcasts location:
POST /drivers/location
Body: { latitude, longitude, status }
Response: 200 OK
Driver accepts a trip offer:
PUT /trips/{trip_id}/accept
Response: { trip_id, pickup_location, rider_name }
Driver updates trip status:
PUT /trips/{trip_id}/status
Body: { status: "in_progress" | "completed" | "cancelled" }
Response: 200 OK
Rider subscribes to real-time driver location:
GET /trips/{trip_id}/live
Upgrade: websocket
Server pushes: { driver_lat, driver_lng, timestamp_ms }
Connection closes when trip status reaches "completed" or "cancelled"
Why HTTP for location updates? At 250K location writes per second, HTTP/2 keep-alive connections amortize connection overhead across many requests. A short-lived HTTP POST per update may add roughly 5ms of latency in this illustrative budget but keeps the driver app stateless: no persistent WebSocket connection to maintain on mobile networks that regularly drop and reconnect. A persistent WebSocket from driver to server can reduce per-update overhead but complicates reconnection logic on unreliable mobile connections. HTTP fits the driver write path here, while WebSocket fits the rider receive path because riders need server-pushed updates without polling.
High-Level Design
1. Rider requests a trip
The write path: the rider submits a pickup/dropoff pair, the Trip Service creates a trip record with status requested, and immediately kicks off asynchronous driver matching. The rider receives a trip_id back without waiting for a driver to accept.
Components:
- Rider App: Mobile client sending the trip request.
- Trip Service: Validates the request, creates the trip record, and records an outbox event for reliable publication to the matching pipeline.
- Trip DB: Stores the authoritative trip record. Status progresses from
requestedthroughaccepted,in_progress, tocompleted.
Request walkthrough:
- Rider app sends
POST /tripswith pickup and dropoff coordinates. - Trip Service validates the locations (valid lat/lng range, reachable geocoordinate).
- Trip Service inserts
{ trip_id, rider_id, pickup_location, dropoff_location, status: "requested", created_at }into Trip DB. - Trip Service records the trip request in the surge demand index:
ZADD trip_requests:cell:{geohash5(pickup_lat, pickup_lng)} {timestamp_ms} {trip_id}on Redis (consumed by the Surge Worker in deep dive 4). - An outbox publisher delivers
TripRequestedEvent { trip_id, pickup_lat, pickup_lng }to Kafka after the trip transaction commits. - Trip Service returns
{ trip_id, status: "requested" }to the rider.
The matching step that consumes the Kafka event is deferred to requirement 3. For now the trip exists in the database, the rider has a trip_id, and the matching pipeline has the event it needs.
2. Drivers broadcast their GPS location
The write path for driver location is separate from the trip request path. Drivers send a GPS update about every 4 seconds regardless of whether they are available, on a trip, or transitioning between states. These updates flow into two destinations: a geospatial index for matching queries, and a real-time channel for active-trip tracking.
Components:
- Driver App: Mobile client sending periodic GPS updates.
- Location Service: Receives driver location updates and writes them to the geospatial index for available-driver queries. When the driver is on an active trip, it publishes GPS positions to Redis Pub/Sub instead (covered in requirement 4).
- Redis Geo (Location Store): A geospatially indexed Redis sorted set. Available drivers are stored here permanently until they accept a trip or go offline.
Request walkthrough:
- Driver app sends
POST /drivers/locationwith current lat/lng and status. - Location Service validates the coordinates and driver status.
- If
status = available: Location Service callsGEOADD drivers:available <lng> <lat> <driver_id>on Redis. - If
status = on_triporstatus = offline: Location Service callsZREM drivers:available <driver_id>to remove from the geospatial index. - Location Service returns 200 OK.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.