Design ride hailing
A rider taps "Request" and within seconds one nearby driver, and only one, gets the trip. Behind that tap: hundreds of thousands of drivers reporting their position every few seconds, an index that answers "who is near this point?" instantly, and a matching step that must never give one driver two trips or one trip two drivers. It follows the interview framework phase by phase.
1. The prompt
"Design Uber: riders request a ride from where they are to a destination, see a price estimate, get matched with a nearby driver, and track the car on a map until the trip ends and payment is taken."
Two very different workloads meet here: a firehose of location updates that only need to be roughly right, and a small number of matching decisions that must be exactly right. Saying this split out loud early is the key to the design.
2. Frame the problem
How many drivers online, and how often do they report location?
About 1M at peak, every few seconds.
~250,000 location writes a second: they can't go to a disk database.
How fast must matching be?
A driver accepts within seconds; the rider sees the match within ~10 s.
Offers go to one driver at a time with a short timeout.
Can a driver ever get two trips at once?
Never. And a request never gets two drivers.
Matching needs an atomic claim on the driver.
Does the rider track the driver live?
Yes, on a map, until pickup and during the trip.
A real-time channel per trip.
Is the price fixed when the rider requests?
Yes: the estimate the rider accepts is honoured.
Fare quotes are stored with an expiry.
Pooled rides, scheduled rides?
Not in this version.
They change matching into an optimisation problem.
One city or many?
Hundreds of cities worldwide.
Partition by city or region: matching is local.
3. What it must do
- A rider can get a fare estimate and request a ride.
- The system matches the request with a nearby available driver, who accepts or declines.
- Rider and driver track each other live, and the trip ends with payment.
Not now:
- Ratings.
- Pooled and scheduled rides.
- Driver onboarding.
- Routing internals: we call a routing service for ETAs.
4. How well it must do it
| Target | Why this number |
|---|---|
| Correct matching: a driver has at most one trip; a request at most one driver consistency | Double booking strands riders and angers drivers. |
| Fast matching: most requests matched within ~10 s speed | Riders give up and try another app. |
| Fresh locations: positions at most a few seconds old speed | Stale positions pick drivers who've already driven away. |
| Scale: 1M drivers online, ~1,200 requests a second at peak load shape | Peaks are local: a concert ends, it starts raining. |
| No lost requests: a request survives a server crash data safety · surviving failures | A rider waiting for a match that silently vanished is the worst experience. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Location updates | 1M drivers ÷ an update every 4 s | 250,000 writes a second, each overwriting the last → so an in-memory geo index, not a disk database |
| Staleness | A car at 50 km/h moves ~13.9 m a second | A 4 s interval means positions are up to ~56 m old: fine for matching |
| Ride requests | Say 20M trips a day, peaks ~5× average | ~230 a second average, ~1,200 at peak: modest, but each needs a correct match |
| Location history | 250,000 × 100 bytes | ~25 MB a second (~2 TB a day) → so a stream for analytics and trip records, off the matching path |
6. The nouns
- Rider.
- Driver: with a status.
- Location: the latest position; only the newest matters.
- Fare quote.
- Ride.
- Offer: a time-limited proposal to one driver.
drivers driver_id, status (offline | available | offered | on_trip), city, vehicle
geo index city cell → {driver_id: (lat, lng, ts)} in memory, TTL ~30 s
fares fare_id, rider_id, pickup, destination, price, expires_at
rides ride_id, fare_id, rider_id, driver_id, status (requested | matched | in_progress | done | cancelled)
locks driver:{id} → ride_id (SET NX with a TTL)
7. The contract
POST /v1/fares {pickup, destination} → {fare_id, price, expires_at}
POST /v1/rides {fare_id} → 202 {ride_id} (matching starts)
POST /v1/drivers/location {lat, lng} (every few seconds; often over a persistent connection)
PATCH /v1/rides/{id} {action: accept|decline} (driver)
WS /v1/rides/{id}/stream → driver position, status changes
The rider or driver comes from the session token. A driver can only update their own location and respond to offers made to them.
8. First working version
A location service writing to a drivers table, a ride service, and a matcher that queries for nearby drivers.
8.1 Reporting locations
Each driver app sends its position every 4 seconds; the location service updates lat and lng on the driver's row.
8.2 Requesting and matching
The ride service saves the request; the matcher queries drivers within a bounding box of the pickup, picks the nearest by straight-line distance, and sends that driver an offer.
8.3 Tracking
The rider's app polls the ride every few seconds for the driver's position.
Parked for later (section 9 fixes each)
- 250,000 row updates a second on a disk database, and a two-column range query to find drivers.
- Two matchers can offer the same driver to two riders at once.
- A matcher crash loses the request mid-offer.
- Straight-line distance isn't driving time.
- Polling for tracking is wasteful and laggy.
- Prices and payments must be exactly right.
9. Make it hold up
9.1 Storing and querying locations fresh locations · scale
| Approach | Why | |
|---|---|---|
| ✗ Bad | A drivers table with lat/lng columns, updated in place; query with WHERE lat BETWEEN … AND lng BETWEEN … | 250,000 updates a second thrash a disk database, and a range on two columns can't use one B-tree efficiently. |
| ~ Good | A spatial index in the database (PostGIS, or a geohash column) | Queries are efficient, but the write rate is still the problem: every update rewrites an index entry on disk. |
| ✓ Great | An in-memory geospatial index sharded by region: map each position to a cell (geohash, Google S2 or Uber's H3 hexagons), keep "cell → available drivers" in memory, with a TTL so drivers who stop reporting disappear | Updates are cheap overwrites; "near me" means look up the rider's cell and its neighbours. Redis GEO commands (GEOADD, GEOSEARCH) do this on a sorted set. Losing the index is fine: drivers report again within seconds. |
Section 13 shows how the update interval trades write load against staleness. Design D14 explores the index structures (geohash grids vs quadtrees) in depth.
9.2 Matching without double booking correct matching · fast matching
- Find candidates: available drivers in nearby cells, ranked by estimated time of arrival (from a routing service, not straight-line distance).
- Offer to one driver at a time with a timeout (say 10 s). Broadcasting to many drivers makes them compete and annoys the losers.
- Lock the driver: before offering, atomically change the driver from available to offered with a short TTL (a conditional update, or
SET driver:42 ride:9 NX PX 10000in Redis). Another rider's matcher then skips this driver. If the driver declines or the TTL expires, the lock is released and the next candidate is tried. - Accept: a transaction moves the ride to matched and the driver to on trip, conditional on the offer still being theirs.
9.3 Not losing ride requests no lost requests
Matching is a multi-step process with timeouts that can last a minute. Run it as a durable workflow (Module 22) or from a queue: the request is written first, and if a matcher crashes, another one resumes the same ride from its saved state. Peaks are local (a concert ends, it starts raining), so partition matchers by region.
9.4 Live tracking fresh locations
Once matched, the rider's app subscribes to the trip's channel over a WebSocket (Module 19). The driver's updates already flow through the location service; for active trips they're also published to the trip channel. Apps smooth the car's movement between updates so it doesn't jump, and drivers report more often during trips than while idle.
9.5 Pricing and trip records correct matching
The fare estimate is computed when requested and stored with an expiry, so the price the rider accepted is the price charged. Surge pricing compares demand and supply per cell, computed by a stream job over requests and available drivers. Trip records, payments and receipts go in a transactional database; payments follow Design D16 (idempotency keys, a ledger).
9.6 Cities, regions and failure scale · surviving failures
- Partition everything that's local (geo index, matchers, surge) by city, so a busy city can't slow another.
- Trips near city boundaries and airport queues need explicit rules.
- If a region's geo index is lost, it rebuilds itself from the next round of driver updates within seconds; matching pauses briefly rather than failing.
- Say the consistency split explicitly: locations can be stale and lossy; assignments and payments must be strongly consistent.
9.7 The whole design after the deep dives
A request matched through the final design.
10. Interviewer follow-ups
A stadium empties: 20,000 requests in 10 minutes from one area.
Matching for that city scales out (partitioned workers), surge pricing brings in drivers, and requests queue durably. Consider batch matching: collect requests for a few seconds and solve a small assignment problem for better overall pickup times.
Pooled rides.
Matching becomes an optimisation: insert a new rider into an existing trip if the detour is small. Candidates are drivers already on compatible trips; scoring uses the routing service. It's still one claim per seat, made atomically.
The routing service is slow.
Use cached ETAs per cell pair, or fall back to straight-line distance with a road factor, so matching continues with slightly worse choices.
A driver's phone loses signal mid-trip.
The trip continues; the app buffers locations and sends them when back. The rider sees "position unavailable" briefly. Fares use the recorded route when it arrives.
How do you test matching changes?
Replay recorded requests and driver positions in a simulator, compare pickup times and acceptance rates, then A/B test by city.
Multi-region failover for a city.
A city's live state (geo index, active offers) can be rebuilt quickly in another region from driver updates; ride and payment records are replicated. Active trips continue; new requests resume after failover.
Fraud: drivers faking GPS.
Check location plausibility (speed between points, roads), compare with network location, and flag outliers for review.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Matcher crashes mid-offer | A few seconds' delay | Workflow step timeout | Another matcher resumes the ride; the driver lock's TTL frees the driver |
| Geo index node lost | Matching pauses briefly in one area | Node health | Rebuilt from the next round of updates |
| Driver ignores an offer | Slight delay | Offer timeout | Lock expires; next candidate offered |
| Lock store unavailable | Matching stops | Lock errors | Fail closed for assignments (never risk double booking); replicated lock store |
| Payment provider slow | Receipt delayed | Payment latency | Payment is asynchronous after the trip; retries with idempotency keys |
12. Wrap-up and the clock
Trade-offs we made:
- An in-memory geo index (fast, lossy) for locations.
- Atomic, TTL-based claims for drivers (correct, simple).
- One offer at a time (fair to drivers, slightly slower).
- Durable matching workflows.
- Partitioning by city.
- Staleness allowed where it's harmless; strong consistency where it matters.
What we'd watch:
- Time to match p90.
- Offer acceptance rate.
- Location age.
- Geo index write rate.
- Lock contention.
- Requests left unmatched.
At 10×:
- More regional partitions.
- Batch matching.
- Adaptive update intervals.
A 45-minute interview
| 0–4 | Frame the problem (driver count, updates, double booking) |
| 4–9 | Must do, how well, numbers (250K writes/s) |
| 9–13 | Nouns and contract |
| 13–20 | First working version, parked list |
| 20–40 | Deep dives: geo index (9.1), locking (9.2), durable matching (9.3), tracking (9.4) |
| 40–45 | Follow-ups and wrap-up |
A 60-minute interview
| 0–5 | Frame the problem |
| 5–11 | Must do, how well, numbers |
| 11–16 | Nouns and contract |
| 16–24 | First working version |
| 24–50 | Deep dives 9.1–9.6 and the final picture |
| 50–60 | Follow-ups: stadium surge, pooling, failover; wrap-up |
| Level | What a strong answer shows |
|---|---|
| Mid-level | Location service, a geospatial lookup, a matching service, a trips database, WebSockets for tracking. |
| Senior | Write-rate estimate driving an in-memory geo index, cell-based lookups, one-at-a-time offers with a TTL lock, durable matching, regional partitioning. |
| Staff+ | Batch matching and marketplace effects, surge computed by stream jobs, multi-region failover for a city, battery and network costs, which data needs strong consistency and which doesn't. |
Common mistakes:
- Locations in a disk database.
- Two-column range queries.
- Broadcasting offers.
- No atomic driver claim.
- Matching state only in memory.
- Treating locations and payments with the same consistency.
13. Try it, check yourself, read more
How often should drivers report their location? Try the numbers. Per-node throughputs are illustrative examples, not benchmarks.
Where should 250,000 driver location updates a second go?
How do you stop two riders being matched to the same driver?
Which data can be stale or lossy without harm?
A matcher crashes halfway through offering a ride. What happens?
| Resource | Why |
|---|---|
| H3: Uber's hexagonal hierarchical spatial index (Uber) | Why hexagonal cells, and how they're used for pricing and dispatch. |
| Redis geospatial (Redis docs) | GEOADD and GEOSEARCH, the building blocks of a simple location index. |
| Design Uber (Hello Interview) | Another walkthrough. |