← System Design
Large-Scale Products

Design a Ride-Sharing Service (Uber)

Watch: "Design Uber w/ a Ex-Meta Staff Engineer" — Hello Interview
In simple words

Drivers keep shouting their location every few seconds into a big city map kept in memory. When you ask for a ride, the system looks at the map squares around you, picks the best nearby driver, and puts a short "reserved" tag on them so nobody else can grab them.

Problem Statement

Design a ride-hailing service like Uber or Ola: riders request a ride, the system finds a nearby available driver, the driver accepts, and both see each other's live location until the trip ends and is paid for. The hard parts are tracking millions of moving drivers and matching quickly without assigning one driver twice.

Functional Requirements

Non-Functional Requirements

Location updates: 5M / 4 s = ~1.25M writes/s  -> in-memory, not a disk DB
Each update ~100 B -> ~125 MB/s ingest; only the LATEST location matters for matching
Ride requests: ~1K/s peak -> matching load is small; location ingest is the big number

Core Entities

API Design

Driver app (persistent connection, e.g. WebSocket/MQTT):
  -> location { lat, lng, heading, ts }          every ~4 s
  <- ride_offer { ride_id, pickup, eta, expires_in: 15 }
  -> accept { ride_id } | decline { ride_id }

Rider app:
  POST /v1/fare-estimate { pickup, dropoff }       -> { fare, eta }
  POST /v1/rides { pickup, dropoff, fare_token }   -> 201 { ride_id, status: REQUESTED }
  GET  /v1/rides/{id}  (+ push updates: matched driver, live driver location)

High-Level Design (HLD)

Tracking drivers and matching a rideDriver appsRider appLocation serviceRide serviceGeo index(in memory, TTL)Matching serviceevery 4 srequestfind drivernearbyoffer (15 s lock)
Latest locations live in memory; matching reserves one driver at a time so nobody is double-booked.
driver apps ==> Location gateway --> Location service
                                        - update geo index (Redis GEO / cells), TTL
                                        - publish to "ride:{id}" channel if on a trip
rider app --> Ride service --(create ride)--> Rides DB
                  |
                  v
             Matching service
               1. query geo index: available drivers within r km of pickup
               2. rank by ETA (routing/ETA service), rating, heading
               3. atomically reserve best driver (status available -> offered)
               4. send offer; wait up to 15 s
               5. accepted -> ride MATCHED; declined/timeout -> release, next driver
             Notification / push -> rider sees driver + live location

Low-Level Design (LLD)

Ride state machineREQUESTEDMATCHEDDRIVER_ARRIVINGIN_PROGRESSCOMPLETED→ PAIDCANCELLEDdriveracceptspickupdrop-offcancel
Each transition is a conditional update on the ride's version, so two events can't both move the same ride.

Geo index for moving drivers

Option A: Redis GEO per city: GEOADD drivers:blr lng lat driver_id
          nearby = GEOSEARCH drivers:blr FROMLONLAT lng lat BYRADIUS 3 km ASC COUNT 20
Option B: cell-based: key = geo cell id (geohash / S2 / H3 at ~1 km)
          cell:{id} -> set of driver_ids; query pickup cell + neighbours

On each update: move the driver between cells if it changed; stale drivers
expire via TTL (an app that stops reporting disappears from matching).
Shard the index by city / region: queries never cross cities.

Assignment without double-booking

-- reserve a driver atomically (Redis or DB); only one matcher can win
SET driver_lock:{driver_id} {ride_id} NX PX 15000     -- offer window
  success -> send offer
  fail    -> someone else is offering this driver; try the next candidate

on accept:  ride.status = MATCHED, driver.status = ON_TRIP (transaction / conditional update)
on timeout: lock expires automatically -> driver available again

Matching can dispatch requests one at a time (nearest good driver) or batch requests over a short window (a second or two) and solve an assignment problem across riders and drivers to reduce total pickup time.

Ride state machine and data

REQUESTED -> MATCHED -> DRIVER_ARRIVING -> IN_PROGRESS -> COMPLETED -> PAID
      \-> CANCELLED (by rider/driver/timeout)

rides(ride_id PK, rider_id, driver_id, status, pickup, dropoff, fare,
      requested_at, matched_at, completed_at, version)   -- updates conditional on version
trip_path: location points during IN_PROGRESS -> appended to a time-series store (for fare & disputes)

Follow-up Questions

Why not store locations in the main database?
1M+ updates per second of data that's only useful for a few seconds. Keep the latest location in memory (with TTL) for matching; persist sampled trip paths separately for fares and history.
How do you compute ETA rather than straight-line distance?
A routing service over the road graph with live traffic (precomputed shortest-path techniques such as contraction hierarchies) ranks the nearest candidates by drive time, since straight-line nearest can be across a river.
A matching server crashes mid-offer. Is the driver stuck?
No: the reservation lock has a TTL and the ride request is persisted, so another matcher picks the ride up and the driver becomes available when the lock expires.

Related