Design Ride Sharing

Design a ride-hailing backend: high-rate location updates, geospatial cells and neighbours, matching without double assignment, and the trip state machine.

Case study32 minLesson 3 of 8
On this page
  1. Clarify the problem
  2. Estimate the scale
  3. Define the API
  4. Model the data
  5. High-level design
  6. Deep dives
  7. Bottlenecks and failures
  8. What a strong answer sounds like
  9. Follow-up questions

You tap "Request" on a street corner. Within a few seconds the app shows a driver two minutes away, and a little car moves along the map toward you. Behind that screen, a million drivers are reporting their position every few seconds, and the system must find the right one near you and make sure no other rider gets the same car.

Interviewers like this question because it has three distinct hard parts that do not appear in request-and-response designs: a write rate far above the read rate, searching by location, and a race between riders for the same driver. The steps follow A Framework for Any Design.

Clarify the problem

The first five minutes
Interviewer

Design a ride-sharing service.

You

I'll focus on the core: a rider requests a ride, we match a nearby driver, and both follow the trip on a map. Are pricing, payments and shared rides in scope?

Interviewer

Leave out payments and shared rides. Assume a price is quoted by another team.

You

How many drivers are online at peak, and how many rides a day?

Interviewer

About a million drivers online at peak, 10 million rides a day.

You

Then my priorities are: a driver is never assigned to two riders, a rider gets a match within a few seconds, and locations shown on the map are at most a few seconds old. Losing one location update is fine; losing a trip record is not, because it is what we bill from.

  • Functional: drivers go online and report location; riders request a ride from a pickup point; the system offers the ride to a nearby driver, who accepts or declines; both see the trip's progress live; trips move through clear states and can be cancelled.
  • Non-functional: no driver on two trips at once (strong consistency for assignment); match within about 5 seconds; locations at most about 5 seconds stale; trip records durable; the system keeps matching even if a location node dies.
  • Out of scope: payments, pricing, shared rides, ratings, driver onboarding.

Estimate the scale

  • Location updates: 1 million drivers, one update every 4 seconds: 106 ÷ 4 = 250,000 writes a second at peak. At about 100 bytes each that is 25 MB/s.
  • Ride requests: 10 million ÷ 86,400 s ≈ 116 a second on average. Assume a rush-hour peak of 3x: about 350 a second. Writes outnumber matching requests about 700 to 1.
  • Latest positions: 1 million drivers × 100 bytes = 100 MB. The whole live map fits in memory on one machine.
  • Trip history: a 20-minute trip with a point every 4 seconds has 1,200 ÷ 4 = 300 points, 30 KB. 10 million trips × 30 KB = 300 GB a day, about 110 TB a year.
  • Riders watching a car move: if each ride has about 5 minutes of waiting and 20 minutes of riding, 10 million × 25 ÷ 1,440 ≈ 174,000 riders are watching at once on average, each needing an update every 4 seconds: about 43,000 pushes a second.
Estimate: Location write rate

How many location updates per second arrive at peak?

You know
  • 1 million drivers online at peak
  • Each driver app sends its position every 4 seconds

Show the working
  1. 106 drivers ÷ 4 s = 250,000 updates a second

Estimate 250 thousand/s (anything from 100 thousand/s to 1 million/s is a good answer)

This is far more than a single relational database should take as row updates, but each update only replaces the last one. That points at an in-memory store that keeps only the latest position.

Your turn: Define the API

Write the driver's location update, the rider's request, and how an offer reaches a driver and gets accepted. How do the apps stay connected?

Compare with a model answer

Drivers keep a persistent connection that carries location updates up and offers down. Riders create a trip with an idempotency key and receive updates on their own connection. Accepting names the offer ID, so a late accept is refused and a repeated one is harmless.

Define the API

Driver and rider API
Driver connection (WebSocket), app -> server:
  location { lat, lng, heading, speed, sent_at }      every 4 s, no reply
  status   { "available" | "offline" }
 
Driver connection, server -> app:
  offer { offer_id, trip_id, pickup, expires_in_s: 15 }
Driver connection, app -> server:
  accept { offer_id }
    -> accepted { trip }  |  refused: offer expired or already answered
 
POST /v1/trips   Idempotency-Key: 7f3a...
  body { pickup, dropoff, quote_id }
  202 { trip_id, state: "MATCHING" }
POST /v1/trips/{id}/cancel     200 | 409 trip already completed
Rider connection, server -> app: trip state changes, driver position

The decision worth arguing is how location updates travel. A fresh HTTP request every 4 seconds from a million phones is 250,000 requests a second, each with headers and often a new connection. A persistent connection carries small messages cheaply, and the server needs it anyway to push offers. Location messages get no reply and are never retried: if one is lost, the next one, 4 seconds later, is fresher than a retry would be. The realtime communication lesson covers the connection servers behind this.

Your turn: Model the data

Which data is disposable and which must be durable? Sketch where driver positions, driver status and trips live.

Compare with a model answer

Latest positions are disposable: keep them in memory, indexed by map cell, and let them expire if a driver goes quiet. Driver status (available, offered, on a trip) decides assignments, so it needs a store with conditional writes. Trips are the durable record in a database.

Model the data

Records
-- in memory, sharded by region; rebuilt from updates within seconds
driver_pos:  driver_id -> { lat, lng, cell, updated_at }   expires after 30 s
cell_index:  cell -> set of available driver_ids
 
-- durable, strongly consistent per row
drivers:     driver_id PK, state,        -- offline | available | offered | on_trip
             offer_id NULL, trip_id NULL, version INT
trips:       trip_id PK, rider_id, driver_id NULL, state, pickup, dropoff,
             created_at, updated_at, version INT
 
-- append-only, partitioned by trip_id
trip_points: (trip_id, sent_at) PK, lat, lng

Three stores for three kinds of data. Positions go in an in-memory store (such as Redis): 100 MB, 250,000 overwrites a second, and nothing lost that the next update will not restore. Driver status and trips go in a relational database or any store with per-row conditional writes, because assignment correctness depends on "update only if the state is still X". Trip points are a high-volume append-only log read by trip ID, which suits a wide-column store (such as Cassandra).

Your turn: High-level design

Draw the location path and the ride request path. Where does matching read from, and where does it write the assignment?

Compare with a model answer

Driver updates flow through a driver gateway into the in-memory geo index. A ride request goes to a trip service, which asks a matching service for candidates from the geo index and claims one with a conditional write on the driver's row. The offer goes back out through the driver gateway.

High-level design

Locations flow into an in-memory geo index. Matching reads candidates from it but claims a driver in the durable driver-state store.Rider app to Trip service (request ride); Driver app to Driver gateway (location / 4 s); Driver gateway to Geo index (update); Trip service to Trips DB; Trip service to Matching service (find driver); Matching service to Geo index (nearby); Matching service to Driver state (claim); Matching service to Driver gateway (offer); Driver gateway to Trip service (trip positions), asynchronous.Rider appDriver appTrip serviceDriver gatewayMatching serviceGeo indexTrips DBDriver staterequest ridelocation / 4 supdatefind drivernearbyclaimoffertrip positions

request and replyasynchronous message

Locations flow into an in-memory geo index. Matching reads candidates from it but claims a driver in the durable driver-state store.
A location update, then a ride requestUse the arrows or ← → keys
  1. A location update, then a ride request, step 1Rider app to Trip service; Driver app to Driver gateway; Driver gateway to Geo index; Trip service to Trips DB; Trip service to Matching service; Matching service to Geo index; Matching service to Driver state; Matching service to Driver gateway; Driver gateway to Trip service, asynchronous.Rider appDriver appTrip serviceDriver gatewayMatching serviceGeo indexTrips DBDriver state
    D7 cell
    dr5ru7

    1Driver D7 sends a position. It is one of 250,000 updates this second.

  2. A location update, then a ride request, step 2Rider app to Trip service; Driver app to Driver gateway; Driver gateway to Geo index; Trip service to Trips DB; Trip service to Matching service; Matching service to Geo index; Matching service to Driver state; Matching service to Driver gateway; Driver gateway to Trip service, asynchronous.Rider appDriver appTrip serviceDriver gatewayMatching serviceGeo indexoverwrite D7Trips DBDriver state
    D7 cell
    dr5ru7

    2The gateway overwrites D7's position in the geo index. D7 is still in cell dr5ru7, so the cell sets do not change; most updates are this cheap.

  3. A location update, then a ride request, step 3Rider app to Trip service; Driver app to Driver gateway; Driver gateway to Geo index; Trip service to Trips DB; Trip service to Matching service; Matching service to Geo index; Matching service to Driver state; Matching service to Driver gateway; Driver gateway to Trip service, asynchronous.Rider appDriver appTrip serviceDriver gatewayMatching serviceGeo indexTrips DBMATCHINGDriver state
    trip
    T1

    3A rider requests a ride. The trip service writes the trip as MATCHING before anything else, so a crash cannot lose the request.

  4. A location update, then a ride request, step 4Rider app to Trip service; Driver app to Driver gateway; Driver gateway to Geo index; Trip service to Trips DB; Trip service to Matching service; Matching service to Geo index; Matching service to Driver state; Matching service to Driver gateway; Driver gateway to Trip service, asynchronous.Rider appDriver appTrip serviceDriver gatewayMatching serviceGeo index9 cells, 14Trips DBDriver state
    trip
    T1
    best
    D7, 2 min

    4Matching reads available drivers in the pickup cell and its 8 neighbours: 14 drivers. It ranks the closest few by driving time, not straight-line distance.

  5. A location update, then a ride request, step 5Rider app to Trip service; Driver app to Driver gateway; Driver gateway to Geo index; Trip service to Trips DB; Trip service to Matching service; Matching service to Geo index; Matching service to Driver state; Matching service to Driver gateway; Driver gateway to Trip service, asynchronous.Rider appDriver appTrip serviceDriver gatewayMatching serviceGeo indexTrips DBDriver stateD7 offered
    trip
    T1
    D7
    offered

    5It claims D7 with a conditional write: set offered only if the state is still available. The write succeeds.

  6. A location update, then a ride request, step 6Rider app to Trip service; Driver app to Driver gateway; Driver gateway to Geo index; Trip service to Trips DB; Trip service to Matching service; Matching service to Geo index; Matching service to Driver state; Matching service to Driver gateway; Driver gateway to Trip service, asynchronous.Rider appDriver appTrip serviceDriver gatewayMatching serviceGeo indexTrips DBDriver stateclaim refused
    trip
    T1
    D7
    offered

    6At the same moment, another matcher working for a rider across the street tries the same claim on D7. The condition fails, so it moves to its next candidate. No double assignment.

  7. A location update, then a ride request, step 7Rider app to Trip service; Driver app to Driver gateway; Driver gateway to Geo index; Trip service to Trips DB; Trip service to Matching service; Matching service to Geo index; Matching service to Driver state; Matching service to Driver gateway; Driver gateway to Trip service, asynchronous.Rider appDriver appoffer, 15 sTrip serviceDriver gatewayMatching serviceGeo indexTrips DBDriver state
    trip
    T1
    D7
    offered

    7The offer goes to D7 through the gateway holding its connection. D7 has 15 seconds to answer.

  8. A location update, then a ride request, step 8Rider app to Trip service; Driver app to Driver gateway; Driver gateway to Geo index; Trip service to Trips DB; Trip service to Matching service; Matching service to Geo index; Matching service to Driver state; Matching service to Driver gateway; Driver gateway to Trip service, asynchronous.Rider appDriver appTrip serviceDriver gatewayMatching serviceGeo indexTrips DBDriver stateD7 on_trip
    trip
    T1
    D7
    on_trip

    8D7 accepts over its connection. Matching moves D7 from offered to on_trip, only if offer o-981 is still live. First of two writes.

  9. A location update, then a ride request, step 9Rider app to Trip service; Driver app to Driver gateway; Driver gateway to Geo index; Trip service to Trips DB; Trip service to Matching service; Matching service to Geo index; Matching service to Driver state; Matching service to Driver gateway; Driver gateway to Trip service, asynchronous.Rider appDriver appTrip serviceDriver gatewayMatching serviceGeo indexTrips DBASSIGNEDDriver state
    trip
    T1
    D7
    on_trip

    9Second write: the trip service moves T1 from MATCHING to DRIVER_ASSIGNED, only if it is still matching. Had the rider just cancelled, this would fail and D7 would be released.

  10. A location update, then a ride request, step 10Rider app to Trip service; Driver app to Driver gateway; Driver gateway to Geo index; Trip service to Trips DB; Trip service to Matching service; Matching service to Geo index; Matching service to Driver state; Matching service to Driver gateway; Driver gateway to Trip service, asynchronous.Rider appDriver appTrip serviceDriver gatewayMatching serviceGeo indexTrips DBDriver state
    trip
    T1
    D7
    on_trip

    10While D7 is on a trip, the gateway also streams its positions to the trip service, which pushes them to the rider every 4 seconds.

  11. A location update, then a ride request, step 11Rider app to Trip service; Driver app to Driver gateway; Driver gateway to Geo index; Trip service to Trips DB; Trip service to Matching service; Matching service to Geo index; Matching service to Driver state; Matching service to Driver gateway; Driver gateway to Trip service, asynchronous.Rider appDriver appdownTrip serviceDriver gatewayMatching serviceGeo indexTrips DBDriver stateoffer expired
    trip
    T1
    D7
    available

    11Another time, the driver's phone is in a pocket and the offer expires. A conditional write returns the driver to available only if the offer is still that one, and matching tries the next candidate.

1 / 11

Deep dives

Interviewers push on four places: the location write rate, how you find nearby drivers, how you stop double assignment, and how a trip's states stay consistent.

Location updates at 250,000 a second

Each update replaces the previous one. That changes everything: you do not need durability, history or replicas for the live positions, only a fast overwrite. If a geo index node dies, its drivers reappear within 4 seconds as their next updates land on the replacement. Say that out loud; it is the reason this part can be simple.

Shard the geo index by region: a city, or a block of cells within a big city. Requests and drivers are local, so a ride request touches one shard. Inside a shard, most updates do not move the driver to a new cell. At 50 km/h a car covers 50 ÷ 3.6 × 4 ≈ 56 metres between updates, and a cell is about 600 metres across, so a driver changes cell at most once every 10 or so updates. The rest are a single overwrite.

Only trip points need to be durable. While a driver is on a trip, the gateway also appends each point to a stream that feeds the trip-points store and the rider's screen. That is 300 points per trip and only for drivers on trips, a fraction of the 250,000 a second.

Finding nearby drivers

"Drivers within 2 km" is slow if you compute the distance to a million drivers. Divide the map into cells, keep a set of available drivers per cell, and look only at the cells near the pickup.

Ways to index driver positions
OptionGood atBad atPick it when
Geohash cells Usual defaultA cell is a short string; nearby points share a prefix. Precision 6 is about 1.2 km × 0.6 km. Easy to shard and to update.Fixed size: a downtown cell may hold hundreds of drivers, a rural one none. Neighbours can have unrelated strings.Constantly moving points with a known search radius. My pick here.
QuadtreeSplits a square into four whenever it holds too many points, so cells adapt to density.250,000 moves a second means constant splitting and merging, which makes it hard to shard and to keep consistent.Points that rarely move, like restaurants or shops.
Hexagon gridEvery neighbour is the same distance away, which makes rings of cells even.Needs a library; harder to explain from scratch in an interview.You need uniform distances, for example in pricing by area.

A geohash interleaves the bits of latitude and longitude and writes them in base 32, so each extra character makes the cell 32 times smaller. Cell sizes shrink away from the equator in the east-west direction, which is a simplification the numbers above ignore.

The trap is the neighbour-cell problem. A rider standing near the edge of a cell may have the nearest driver 50 metres away, across the border, in a cell with a completely different string. Searching only the rider's cell misses that driver. Always search the cell plus its 8 neighbours: 9 × 1.2 × 0.6 ≈ 6.5 km². If that returns too few drivers, step out to the next ring or to a coarser precision. Straight-line distance is only a filter: a driver 300 metres away across a river may be 10 minutes out, so rank the top few by driving time from a routing service.

Matching without double assignment

Two riders across the street from each other both see driver D7 as the closest. If both matchers read "available" and both write "assigned", D7 gets two trips. This is a classic read-then-write race, and it is the part interviewers care about most.

Ways to stop two riders getting one driver
OptionGood atBad atPick it when
Conditional write on the driver row Usual defaultOne atomic "set offered if available". Only one writer can win; the loser retries with the next driver. No extra component.The store must give linearisable single-key writes. A store that accepts concurrent writes on several replicas and resolves them later by last-write-wins would let both claims succeed.Almost always. My default.
One matcher per regionAll matches for a region run in one place, one at a time, so no race is possible. Can match several riders jointly.That process is a bottleneck and a single point of failure; borders between regions still need care.Dense regions where batching requests every second or two gives better matches.
Distributed lock per driverFamiliar pattern.A lock that expires while its holder is paused can be taken by another, so you still need a check at write time. That check alone would have done the job.Rarely. Prefer the conditional write.
Claiming a driver
UPDATE drivers
SET    state = 'offered', offer_id = 'o-981', version = version + 1
WHERE  driver_id = 'D7' AND state = 'available';
-- 1 row updated: we own the offer.  0 rows: someone else got there first.

The precise guarantee to state: the conditional write must run against a single authority for that row, a leader or a consensus group. Quorum reads and writes on their own do not make "read, check, write" atomic; two clients can each read "available" from a quorum before either writes. The condition has to be checked and applied as one operation.

The offer itself is a lease with a 15-second expiry. Accepting checks the offer_id, so a retried accept is harmless and a late one is refused.

Accepting is two conditional writes in two stores, in a fixed order: the driver row (offered to on_trip, only if the offer is still live), then the trip (MATCHING to DRIVER_ASSIGNED, only if still matching). It is not one transaction. If the trip write fails because the rider cancelled, release the driver. If a crash falls between the two writes, the driver app retries the accept with the same offer_id, and a sweeper releases any driver whose trip_id points at a trip that is not theirs. Cancelling and completing release the driver the same way.

The trip state machine

A trip passes through a fixed set of states, and many actors can try to change it at once: the rider cancels while the driver accepts, or a timeout fires as the driver taps "arrived". Write the allowed moves down and refuse everything else.

FromToTriggered by
MATCHINGDRIVER_ASSIGNEDdriver accepts a live offer
MATCHINGNO_DRIVERSno accept within 60 seconds
DRIVER_ASSIGNEDDRIVER_ARRIVEDdriver taps "arrived" near the pickup
DRIVER_ARRIVEDIN_PROGRESSdriver starts the trip
IN_PROGRESSCOMPLETEDdriver ends the trip
any state before IN_PROGRESSCANCELLEDrider or driver cancels

Each move is a conditional update on (trip_id, state, version). If the rider cancels and the driver accepts in the same instant, exactly one of those updates finds the expected state; the other gets a conflict and the app shows the true state. Every change is also published as an event, read from the database's change log so none is lost between write and publish. Events drive both apps and billing.

Bottlenecks and failures

At 10x (10 million drivers, 2.5 million updates a second) the geo index needs more shards, split by smaller regions; a single dense city may need splitting by cell prefix. Each shard still holds little data, so the limit is update throughput, not memory. Matching grows to about 3,500 requests a second at peak, and driver-state writes grow with it; partition driver state by region so claims stay local.

When this diesWhat happensWhat you do
A geo index shardNo candidates in that region for a few seconds.Route updates to a standby; it fills itself from the next round of updates in about 4 seconds.
A driver gateway serverIts drivers lose their connection.Apps reconnect to another server; offers in flight expire and go to the next driver.
The matching serviceNew requests wait in MATCHING.Run several stateless matchers; the conditional write makes them safe to run in parallel.
The driver-state primaryNo new claims can be made.Fail over to a replica that has every committed write (synchronous replica or consensus); matching pauses briefly rather than risk double assignment.
A driver's phoneUpdates stop.The position expires after 30 seconds and the driver drops out of the candidate sets.

What a strong answer sounds like

LevelWhat the answer does
JuniorStores driver positions in a database table and queries by distance; handles assignment once the race is pointed out.
MidEstimates the update rate, keeps positions in memory, uses geohash cells and remembers the neighbouring cells.
SeniorSeparates disposable positions from durable assignments, claims drivers with a conditional write and explains what store guarantee that needs, models the trip as a state machine with conditional transitions, and ranks candidates by driving time.
Defending the matching designStrong answer
Interviewer

Two matchers pick the same driver. What happens?

You

Both try the same conditional update: set the driver to offered only if they are still available. The store applies those one at a time on that row, so one gets "1 row updated" and the other gets 0 and moves to its next candidate. That only works if the row has a single authority, a leader or a consensus group. A store that accepts writes on several replicas and merges later would let both succeed, so driver state does not go there.

Follow-up questions

How would you do surge pricing?

Count open requests and available drivers per cell every minute from the same index and events, and publish a multiplier per area. The pricing team owns the formula; the design only has to supply fresh counts.

How do you estimate arrival time?

Use a routing service over the road graph with live traffic, a shortest-path search like Dijkstra's algorithm with heavy precomputation. Only call it for the handful of candidates that pass the straight-line filter.

Could you match better by waiting?

Yes. In dense areas, collect requests for one or two seconds and assign them together to minimise total pickup time. It trades a little latency for better matches, and fits the one-matcher-per-region option.

How do riders see the car move smoothly?

The server pushes the driver's position every 4 seconds over the rider's connection, and the app animates between points along the road.

What if a driver's GPS jumps a kilometre?

Reject updates that imply an impossible speed, and snap positions to the road network before using them for matching or billing.

Self-review: ride sharing0 of 12

After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".

Before drawing

Numbers and interfaces

Design and depth

Failure