Ride-Sharing Dispatch & Matching System Design
A system design interview walkthrough for an Uber-style dispatch system matching riders to nearby drivers at scale — geospatial indexing, batched matching, real-time location ingestion, and the surge-pricing feedback loop.
Finding the nearest available driver sounds like a database query. At the scale of a real ride-sharing platform, it's a two-sided marketplace problem where both sides are moving, both sides are arriving continuously, and a locally "correct" match can still be a globally bad one. This post walks through the design the way an interview conversation actually unfolds.
Requirements and Goals of the System
Functional Requirements
- A rider requests a ride and gets matched to a nearby, available driver
- Driver location updates in near-real time as they move
- Estimated price and ETA are shown before the ride is confirmed
- Pricing adjusts during periods of high local demand (surge)
Below the line (out of scope)
- Payment processing — a downstream system that reads the ride's final fare
- Turn-by-turn navigation — we need distance/ETA estimates, not full routing; a maps provider handles the rest
- Driver onboarding and background checks
Non-Functional Requirements
- Scale: roughly 1M concurrent active drivers, each sending a location update every few seconds — call it ~250,000 location writes/second platform-wide
- Latency: a rider should be matched with a driver within a few seconds of requesting
- Consistency: driver location can be a few seconds stale without meaningfully hurting match quality — this is a case where eventual consistency is not just acceptable but the right trade-off
- Marketplace quality: the goal isn't just "assign a driver," it's minimizing rider wait time and driver idle time in aggregate across a whole city
Capacity Estimation and Constraints
Location ingestion: ~250,000 writes/second platform-wide, each a small payload (~100 bytes: driver ID, lat/lng, timestamp) — roughly 25MB/second of raw ingestion. This is the dominant traffic pattern in the whole system, and it's overwhelmingly writes that immediately supersede the previous value, not data that needs to accumulate.
Ride requests: a much smaller number by comparison — say 10 million rides/day globally averages to only ~115 requests/second, with peaks perhaps 5x that around commute hours and events. Ride records themselves are lightweight (~1KB each including state transitions), so daily storage for ride data is on the order of 10GB/day — trivial next to the location-ingestion volume.
The number worth sitting with: location writes outnumber ride requests by roughly 1,000:1. Any design that routes both through the same storage tier with the same durability guarantees is solving the wrong problem for at least one of them — which is exactly why they end up in architecturally different stores below.
System APIs
High-Level Design
The core requirement to design around: match a rider to a nearby, available driver within a few seconds, across a million moving drivers.
Bad solution: on every ride request, scan the full driver table, compute the distance from each driver to the pickup point, and pick the closest available one.
This is an $O(n)$ scan against the entire fleet on every single request. With a million drivers, doing that computation per request — and doing it again for the next request a moment later — doesn't come close to a multi-second matching budget, and it only gets worse as the fleet grows.
Good solution: index driver locations geospatially — geohashing or a quadtree — so a ride request only needs to search drivers within a small radius of the pickup point, not the entire fleet. A geohash prefix lookup against an in-memory store like Redis turns "find nearby drivers" into a fast, bounded query instead of a full scan.
This makes a single match fast, but exposes a subtler problem: it matches one ride request at a time, greedily. At real scale — a stadium letting out, a big event ending — many ride requests land within the same second in the same area, and assigning the closest driver to whichever request happened to arrive first isn't necessarily the assignment that minimizes total wait time across all of them.
Great solution: batch the matching. Instead of matching each request the instant it arrives, collect ride requests and available drivers within a short window (a couple of seconds) for a given region, and solve the assignment across all of them jointly — closer to a bipartite matching problem than a sequence of independent nearest-neighbor lookups. This adds a small, deliberate delay before a match is confirmed, in exchange for a better aggregate outcome: fewer riders paired with a distant driver just because they happened to ask first. Partitioning the world into regions (roughly by city or geohash cell) keeps each matching batch's problem size bounded and lets the whole system scale horizontally, region by region, instead of running one global optimization.
Database Design
- Rider — the person requesting a ride
- Driver — the person fulfilling it; a mostly-static profile plus a constantly-changing current location
- Ride — the request itself, moving through a state machine: requested → matched → in progress → completed/cancelled
- DriverLocation — the current geo-position of a driver, updated every few seconds, deliberately modeled apart from the Driver profile
- Zone — a geographic region (roughly a neighborhood-sized cell) used to compute local supply/demand and surge pricing
The modeling choice worth explaining if asked: DriverLocation is not a field on the Driver row, and it doesn't live in the same durable store. It's overwritten every few seconds for a million drivers, which is a write pattern a transactional database handles poorly and a fast in-memory geospatial index handles well — while the Ride state machine is exactly the opposite: infrequent, high-value transitions where every state change is worth keeping as an immutable event, since support and disputes need to reconstruct exactly what happened to a specific ride.
Detailed Component Design
Why batch matching instead of matching instantly?
Instant, greedy matching optimizes each request in isolation and can leave the marketplace worse off in aggregate — the first request to arrive grabs the nearest driver even when a slightly different pairing would have gotten both waiting riders picked up sooner. A short batching window turns matching into a small assignment problem solved jointly across the requests and drivers currently pending in a region, which is what actually improves average wait time and driver utilization — at the honest cost of a couple of seconds of added latency before a rider sees a match, which is a trade-off worth naming explicitly rather than assuming instant is always better.
What stops two ride requests from matching to the same driver at once?
This is a real race condition, not a hypothetical one: two nearly-simultaneous requests can both query the geospatial index, both see the same driver as the closest available option, and both attempt to confirm a match. The fix is to make "reserve this driver" an atomic operation — a compare-and-swap on the driver's status from available to pending-match — rather than a read followed by a separate write. Whichever request wins the atomic reservation gets the driver; the other falls through and gets matched to its next-best option.
How does surge pricing interact with matching, and why can it become unstable?
Surge pricing is typically computed per zone from the ratio of open ride requests to available drivers, and applied at request time before a rider confirms. The instability risk worth naming: if price recalculates too frequently or too sharply in response to short-lived imbalances, it can oscillate — a price spike suppresses demand and pulls in more drivers, which then over-corrects the price back down, repeating. Smoothing the input (a rolling window rather than an instantaneous ratio) and rate-limiting how often price can change in a zone keeps the feedback loop stable rather than jittery.
Fault Tolerance and Replication
| Failure | Impact | Response |
|---|---|---|
| Geospatial index node loss | Drivers indexed on that node briefly disappear from search | Because every driver republishes their location every few seconds, the index is self-healing — a lost node's data reappears within one ingestion cycle. Real replication is still used for read availability, but the write side tolerates loss gracefully by design |
| Matching service crash mid-batch | A pending batch of requests and drivers needs to be re-run | Matching is stateless per batch — a crash just means the next instance re-collects whatever requests are still open and re-runs the assignment; no ride is silently dropped since requests remain in the requested state until matched |
| Ride state store failure | Risk of losing an in-progress ride's status | Unlike location data, Ride records are durable, low-volume, high-value writes — this store is replicated synchronously across availability zones, since losing an in-progress ride (rider already picked up) is a real customer-facing incident, not a recoverable blip |
| Regional partition isolated from the rest | A city's matching temporarily can't borrow drivers from neighboring regions | Acceptable in the short term, since ride-sharing supply and demand are inherently local — a region operating in isolation for a few minutes rarely degrades match quality meaningfully |
The pattern mirrors the data model split: ephemeral, self-healing location data can tolerate loss because it's constantly being refreshed anyway; durable ride state cannot, and is engineered accordingly.
Extended Requirements
Observability & Monitoring
| Signal | Why it matters | Alert on |
|---|---|---|
| Time-to-match (P50/P99) | The core promise to the rider | Sustained rise vs. baseline for a region |
| Location ingestion lag | Stale locations directly degrade match quality | Lag exceeding a few seconds at the geospatial index |
| Match-then-cancel rate | A high rate often means the matched driver was further away than expected (stale or sparse location data) | Spike vs. regional baseline |
| Surge price volatility, per zone | Flags an unstable feedback loop before riders notice erratic pricing | Price flips beyond a set bound within a short window |
Security & Compliance
Driver-submitted location is a self-reported signal, which means it can be spoofed — a driver claiming to be in a high-surge zone they're not actually in is a real abuse pattern, not a theoretical one, and worth cross-checking against plausible movement speed between consecutive updates. Location data is also sensitive personal information for both riders and drivers; access to raw location history should be scoped tightly and retention of precise historical pings should be minimized once a ride is complete, rather than kept indefinitely by default.
Leveled Expectations
Mid-level: Gets to a geospatial index for nearby-driver lookup and a basic ride state machine, with some prompting toward why a full table scan doesn't work.
Senior: Independently identifies the greedy-matching-at-scale problem and proposes batched/windowed matching; correctly separates ephemeral location data from durable ride records; catches the double-booking race condition and proposes an atomic reservation.
Staff+: Frames matching explicitly as an assignment/optimization problem, not a lookup problem; reasons about surge pricing as a feedback system that can become unstable, not just a pricing formula; discusses regional partitioning as the mechanism that lets the whole system scale horizontally rather than treating matching as a single global service.
Further Reading
- Alex Xu — System Design Interview Vol. 2 (chapter on proximity/nearby services)
