How an Uber-like ride-hailing platform might evolve
Hundreds of thousands of moving cars report their position every few seconds, and a rider expects a match in seconds. How location ingestion, geospatial indexing, dispatch, surge pricing and trip consistency shape a real-time marketplace.
The user problem and the hard parts
A rider wants a car, nearby, now, at a price they can see up front. A driver wants a steady stream of nearby trips. The platform sits between them and must decide, many times per second per city, who gets which trip.
The hard engineering problems:
- Location ingestion: every active driver app sends a GPS point every few seconds. 500k online drivers at one update every 4 s is about 125k writes per second, nearly all of which are overwritten moments later.
- Geospatial indexing: "which available drivers are within 3 minutes of this pickup?" must be answered in milliseconds, without scanning every driver in the city.
- Dispatch matching: the nearest car is not always the best one. Matching a batch of riders and drivers together can reduce total waiting time.
- Surge pricing: when demand exceeds supply in an area, prices rise to attract drivers and ration demand. Computed per area, every minute or so.
- Consistency of trips and payments: a driver must never be assigned two trips at once; a rider must be charged exactly once, even if the phone loses signal at drop-off.
How the architecture might evolve
A plausible architecture for a ride-hailing platform at each scale. Uber has publicly described its H3 hexagonal geospatial index, which is mentioned as such; the rest is illustrative.
- 1
Stage 1 · One city
500 drivers · 2k trips/dayRider and driver mobile appsMonolith APIPostgreSQL with a geospatial extensionPayment providerPush notificationsA geo index in the main database answers "drivers near me" fine at this size. One database keeps trip state and payment state consistent with ordinary .
- 2
Stage 2 · Many cities
50k drivers · 500k trips/dayLocation service with in-memory store (Redis)Persistent connections to driver appsDispatch service per cityQueue + workers for receipts and payoutsRead replicasLocation updates move out of PostgreSQL into memory, keyed by driver, with a short TTL (time to live). Dispatch becomes its own service because its latency budget is very different from the rest of the API.
- 3
Stage 3 · National scale
500k drivers · 10M trips/dayHexagonal grid geospatial index (Uber has published H3)Kafka stream of location and trip events, partitioned by city/cellBatch matching every few secondsSurge pricing computed per cell from the event streamTrip state machine service + ledger for paymentsPartitioning by geography lets dispatch, location and pricing scale horizontally: each city or cell range is handled by a subset of nodes. Streaming events let pricing, ETAs and fraud detection consume the same data independently.
- 4
Stage 4 · Global platform
Millions of drivers · multiple products (rides, food, freight)Region-local dispatch with global identity and paymentsSharded trip storageML-based ETA and matchingMulti-region failover for the trip pathShared marketplace platform reused by several productsDispatch is inherently local (a driver in Paris never matches a rider in Lima), so it runs close to its city. Accounts and payments are global and need stricter consistency, so they are separate systems with their own replication strategy.
From GPS ping to matched trip
A plausible architecture for Stage 3 might look like this. Follow a location update and a ride request until they meet.
Click any component to see what it does.
Deep dive: why hexagons, and why batch matching
Geospatial cells. The trick is to turn a 2D search into key lookups. Cover the map with cells, give each cell an id, and index drivers by cell id. "Nearby" becomes "this cell plus its neighbours". Square grids and geohashes work, but squares have neighbours at two different distances (edges and corners). Hexagons have six equidistant neighbours, which makes "rings of distance" uniform; that is one reason Uber publicly describes building H3, a hierarchical hexagonal index.
Greedy vs batch matching. Greedy: each request takes the nearest free driver immediately. Simple, but two requests a block apart can steal each other's ideal driver. Batch: collect requests for one or two seconds and solve them together to minimise total pickup time. Batching adds a little latency to each request but improves the whole city's efficiency.
| Data | Volume | Consistency | Storage |
|---|---|---|---|
| Driver location | ~100k writes/s | Latest-wins, lossy | In-memory index + stream |
| Surge multiplier | Per cell per minute | Eventual | Cache, from stream |
| Trip state | Hundreds/s | Strong per trip | Transactional store, sharded by trip/city |
| Payments | Hundreds/s | Exactly once, auditable | Ledger + payment provider |
Engineering trade-offs
Store locations in the main database
- One system, simple queries
- Durable history for free
- Write firehose competes with trips and payments
- Index churn on every update
In-memory geo index fed by a stream
- Millisecond lookups
- Scales by partitioning geography
- Stream doubles as input for pricing and analytics
- Data is lossy on node failure (rebuilt from the next pings)
- More moving parts
Greedy nearest-driver matching
- Lowest per-request latency
- Easy to explain and debug
- Globally inefficient in dense areas
Batch optimisation matching
- Lower average pickup time city-wide
- Room for business rules (driver fairness, vehicle types)
- Adds seconds of delay
- Harder to reason about individual outcomes
Practical exercise
The tunnel at drop-off
A driver ends a trip inside a tunnel. The app taps "Complete trip", the request times out, and the app retries three times when signal returns. Meanwhile the rider's app shows "Trip in progress". How do you guarantee the trip is completed once and the rider charged once?