Design a ride-sharing service (Uber-style)
Expected question
"Design a ride-sharing service (Uber-style). How do you match riders and drivers, maintain accurate ETAs, price rides, and keep trip state correct at city scale?"
Variant forms
Same design, different framing:
- "Design Uber for a city with 1M daily rides and 100k concurrently online drivers."
- "How do you find the nearest available driver without querying every driver?"
- "Design dispatch for ride requests where drivers continuously move."
- "Our pickup ETAs are inaccurate during rush hour — how would you improve them?"
- "How do you calculate surge pricing without creating a feedback loop?"
- "Design driver location updates at 1–5 second intervals."
- "How do you prevent two riders from being assigned the same driver?"
- "Design the lifecycle from request through cancellation, completion, and payment."
Where this actually gets asked
Canonical marketplace / real-time-location system-design prompt in consumer marketplace and logistics interviews. It tests geo indexing, asynchronous workflows, and product trade-offs more than perfect route planning. Staff+ depth: partitioning cities, dispatch correctness, price explainability, and degraded behavior when location or maps are stale.
Executive summary
30-second thesis
I'd keep trip state in a durable conditional state machine and put live drivers in a city-sharded geo index — matching is offer+CAS, not "notify everyone nearby."
2-minute answer
Clarify city scale, products, and whether pooling is in scope (usually later). Default: trip created with idempotency key → dispatcher queries geo cells for eligible drivers → sequential or small-batch offer with short lease → accept does UPDATE … WHERE state=SEARCHING AND version=?. Location stream is high-rate and ephemeral (~33k events/sec at 100k drivers × 3s H); don't write every ping to OLTP. ETA from road+freshness, not crow-flies alone; surge from smoothed supply/demand with quote pinned at booking. City partition for dispatch; DR replicates trips, not active driver locks cross-region. Maps down → low-confidence straight-line ETA labeled as such. In 45 minutes: geo candidates + CAS assign before ML dispatch fantasies.
Quantitative trade-offs
| Decision | Trade-off and reversal evidence | Evidence class |
|---|---|---|
| Sequential offer vs batch broadcast | Sequential cuts races; reverse to small batch when accept rates are poor and latency budget allows. | H |
| Fine geohash vs coarse cells | Fine improves candidates; reverse when border thrash / query fanout dominates. | H |
| Pin fare quote vs live surge | Pinning protects trust; reverse only with explicit product rules for requote. | H |
What I'd ask them
- Single city or multi-city day one? Pooling in scope?
- Driver location cadence and max match latency?
- What happens on payment auth failure after assignment?
Requirements
Functional
- Riders request, cancel, track, and pay for a trip.
- Drivers go online/offline, publish location, accept/decline, and complete trips.
- Match an eligible nearby driver and return pickup ETA and fare estimate.
- Calculate dynamic pricing; notify both sides of state changes.
Non-functional
- Driver location and trip state propagate in seconds; rider updates feel real time.
- Never assign one driver to two active trips; keep a durable trip history.
- Handle city-level bursts and localized hotspots; P99 matching under a few seconds.
- Protect precise location and payment data; tolerate delayed map or notification dependencies.
Core entities
- Driver: driver_id, availability, vehicle, current_cell, location_ts, eligibility.
- Rider: rider_id, payment_profile, rating, current_trip_id.
- Trip: trip_id, rider_id, driver_id, state, pickup, destination, quoted_fare, version.
- Location event: driver_id, lat/lon, heading, timestamp, sequence.
- Supply/demand window: geo_cell, service_type, time bucket, requested, available, multiplier.
API / interface
POST /v1/trips
Idempotency-Key: 6ed...
{ "pickup":{"lat":37.78,"lon":-122.41}, "destination":{"lat":37.76,"lon":-122.43}, "product":"standard" }
→ 202 { "trip_id":"t_123", "state":"SEARCHING", "fare_quote":{"min":14,"max":18,"currency":"USD"} }
POST /v1/drivers/me/location
{ "lat":37.781,"lon":-122.412, "heading":90, "seq":812 }
→ 204
POST /v1/trips/t_123/accept
{ "trip_version":3 }
→ 200 { "state":"DRIVER_EN_ROUTE" }
GET /v1/trips/t_123
→ 200 { "state":"DRIVER_EN_ROUTE", "driver_location":{...}, "eta_seconds":180 }
Staff+ callout: acceptance is a conditional state transition, not a best-effort notification; use
trip_version or a transactional compare-and-set to reject a stale second accept.
Data Flow
The rider request is durably created, then a city dispatcher finds candidates from a geo index and offers sequentially or in a controlled batch. Location is a high-rate ephemeral stream; trip events are durable and drive notifications, payment, and analytics.
Rendering architecture diagram…
High-level design
a control plane owns durable trips and assignment, while a regional real-time plane maintains driver availability and location. Payments and notifications consume trip events rather than extending dispatch latency.
Rendering architecture diagram…
Deep dive 1: geo index and location ingestion
At 100k active drivers sending one update every 3 seconds, ingest is about 33k events/sec before replication. Store the latest location in a memory-oriented geo service, not the trip OLTP database. Geohash cells are easy to shard and query by expanding neighboring cells; a quadtree adapts better to dense downtown areas. Use a cell precision around 5–7 (hundreds of meters to a few km), then rank exact coordinates by road ETA.
| Choice | Strength | Cost / risk |
|---|---|---|
| Geohash grid | Simple key partitioning and neighbor lookup | Uneven density and border expansion |
| Quadtree | Splits dense cells adaptively | More complex rebalance and routing |
| Road-network index | Better candidate quality | Expensive; still needs coarse prefilter |
Discard out-of-order seq events, mark drivers stale after e.g. 15 seconds, and use a TTL so dead
clients leave supply automatically. Location history goes asynchronously to cold storage with access
controls; dispatch only needs the latest point.
Deep dive 2: matching and the trip state machine
Expand search rings until enough candidates exist, filter by product, vehicle capability, driver state, and route direction, then rank by predicted pickup ETA rather than crow-flies distance. Offer one driver first for fairness and lower duplicate work, or a small batch with short leases when acceptance is poor. A 10-second offer lease expires safely.
REQUESTED → SEARCHING → ASSIGNED → DRIVER_EN_ROUTE → ARRIVED
→ IN_PROGRESS → COMPLETED → PAID
↘ CANCELLED
The authoritative transition is UPDATE trips SET driver_id=?, state='ASSIGNED', version=version+1 WHERE id=? AND state='SEARCHING' AND version=?. If it affects zero rows, the driver lost a race or
the rider cancelled. Publish an outbox event in the same transaction; consumers are at-least-once
and deduplicate by trip/event id.
Deep dive 3: ETA and surge pricing
Route engines provide a baseline travel time; ETA models add recent road speeds, pickup friction, driver heading, weather, and map confidence. Recompute on meaningful movement or every 15–30 seconds, not on every GPS point. Show a range when uncertainty is high.
Surge uses smoothed supply/demand by city cell and product over short windows, with minimum sample sizes and caps. Avoid pricing off raw instantaneous counts: driver app updates and rider retries can create oscillation. Quote and persist the multiplier/version at booking; a later surge change must not silently alter a rider's accepted quote.
Deep dive 4: failure modes and multi-region scope
Partition operationally by city/region so dispatch stays local; replicate durable trip records for DR, but do not synchronously coordinate a driver between distant cities. If the geo index is lost, rebuild from live location heartbeats and temporarily widen search. If maps are down, use straight-line fallback ETA labeled low confidence. If push delivery fails, apps poll trip state.
Prevent split-brain availability with a driver session lease owned by one city shard. A driver crossing a boundary transfers only when idle, or the active trip retains its original shard. In 45 minutes, make state transitions and geo candidate lookup explicit before discussing ML dispatch optimization.
What's expected at each level
- Mid-level: store drivers, find nearby locations, describe request/accept/complete.
- Senior: geo cells, WebSockets, trip states, durable events, basic ETA and surge.
- Staff+: conditional assignment, stale-location handling, city partitioning, quote semantics, event/outbox consistency, and measurable latency budgets.
- Principal: marketplace incentives, cross-city operations, fairness, safety/compliance, and how dispatch, pricing, and supply programs are governed together.
Follow-up questions to expect
- "Why not blast every nearby driver?" — Burns attention, creates accept races, and tanks fairness. Offer with a lease.
- "Driver accepts while rider cancels?" — Conditional transition picks one winner. The loser gets a clean rejection, not a half-assigned trip.
- "Pooling?" — Route-compatible matching; state and ETA get much harder — call it a follow-up design.
- "How accurate must location be?" — Track confidence/staleness; never show false precision.