Why ride hailing is a classic design question, and what it teaches
A ride-hailing service matches riders who want a trip with nearby drivers, tracks the trip, and charges for it. Locations change every few seconds, a driver can take only one ride at a time, and both sides expect an answer quickly. It teaches location data, matching, state machines and handling people who do not respond.
It gives you two things interviewers always ask about:
- Low latency matching: a rider should be matched within seconds, using fresh locations.
- Correctness under concurrency: a driver must never be given two rides, and a ride must never have two drivers, even with many requests at once.
Requirements to confirm first:
- Functional: request a ride, find nearby drivers, offer, accept, track the trip live, fare and payment, ratings.
- Non-functional: matching in a few seconds, location updates from many drivers every few seconds, high availability in each city.
Skip the design, or simplify it, when:
- You only dispatch a small fleet by hand: a table and a map are enough.
- Locations can be minutes old: ordinary requests and a database are enough.
Rough capacity (illustrative numbers)
These figures are assumptions for one busy city, to show the method. Replace them with the real numbers you are given.
- Location writes: 100,000 drivers online, one update every 4 seconds, is 100,000 / 4 = 25,000 updates per second.
- Ingest bandwidth: at about 100 bytes per update, that is 25,000 x 100 = 2.5 MB per second. Keeping every point for a day would be 2.5 MB x 86,400 = about 216 GB, which is why history goes to cheap storage and not the main database.
- Index size: the latest position of 100,000 drivers at about 50 bytes each is 5 MB, so the index fits easily in memory. The limit is write rate and search rate, not size.
- Matching load: 200 ride requests per second at peak, with offers to 3 drivers each when broadcasting, is 600 offers per second. That is small next to the location writes, so the location path is the part to scale first.
Try it
A rider requests a trip with four drivers nearby. Change how offers are sent and what goes wrong.
Ride Hailing
Step 1 of 6 · Press Space to play, arrow keys to stepInitial state. A rider requests a ride. Four free drivers are nearby, sorted by distance.
- tick:
- 0
- offers sent:
- 0
- wait for driver (ticks):
- -
- ride:
- requested
1 tick = one step of the simulation.
Things to try:
- Run Offer to the nearest driver with no failure. One offer, one acceptance.
- Choose Nearest driver does not answer. The rider waits for the timeout before the next offer.
- Switch to Offer to the 3 nearest with the same failure, then try Driver accepts, then cancels.
Dispatch strategies
| Strategy | How it works | Good when | Weakness |
|---|---|---|---|
| Nearest driver, one at a time | Offer to the closest driver, wait, then try the next | Few riders and plenty of drivers | A slow or absent driver delays the rider |
| Broadcast to several | Offer to a few nearby drivers, the first to accept wins | Fast matching matters | Drivers see offers they will not get, and the server must pick one winner |
| Batch matching | Collect requests for a few seconds and assign globally | Busy areas | Adds a short wait but gives better overall pickups |
| Pre-assigned zones | Drivers wait in a queue per area | Airports and stations | Not the nearest driver |
Location tracking vs trip state
- Location tracking is a firehose of small updates. Drivers send positions every few seconds. You only need the latest position, so store it in a fast in-memory index and do not keep every point in the main database. Keep history in cheaper storage for later analysis.
- Trip state is a small amount of data that must be correct. A ride moves through states (requested, matched, arrived, started, completed, cancelled) and each change must be atomic.
Treat them separately: fast and approximate for locations, strict for trips.
Two terms used below. A spatial index stores points so that "who is near here?" looks at a few nearby cells (a geohash grid, a quadtree or Redis GEO) instead of every driver. An atomic update is one step that either fully happens or does not happen at all, with no other request able to slip in between the check and the change.
How matching works
- The rider requests a ride, and the service creates a ride record in state
requested. - It queries the location index for free drivers within a radius, sorted by distance.
- It sends an offer to a driver through their open connection or a push message, and sets a timeout.
- When a driver accepts, the service performs one atomic update: change the ride to
matchedwith this driver only if it is stillrequested, The driver is claimed just before this step (see "Guarding both sides" below). If another driver won first, the update changes nothing and the later acceptance is told it is too late. If the ride is already matched to this same driver, treat a retry as success. - If the offer times out or is declined, it moves to the next driver.
- The trip then streams the driver's location to the rider until completion.
Guarding both sides. The conditional update on the ride stops a ride having two drivers. It does not by itself stop one driver accepting two rides at once. Claim the driver first with its own conditional step (for example a driver row updated from free to busy only if it is still free, or a short lock on the driver), then update the ride, and release the driver claim if the ride update fails. Treat the ride database as the source of truth. The location index is only a hint for finding candidates, and it is a different store, so the two cannot be changed in one transaction. Give the driver claim a short lease (an expiry), so a matcher that crashes between the two steps does not leave the driver busy for ever.
A race, step by step. Drivers A and B both accept the same ride within a few milliseconds. With check-then-update, both read requested, both write, and the last write wins, so one driver drives to a rider who is already taken. With one conditional update, A's update changes one row and B's changes none, so B is told the ride is taken and goes back to the free pool.
Ride hailing design vs the alternatives
| Concern | Real-time matching service | Manual dispatch | Fixed schedules |
|---|---|---|---|
| Matches in seconds | Yes | Slow | No |
| Handles thousands of drivers | Yes | No | Yes |
| Prevents double booking | Yes, atomic update | By hand | Yes |
| Uses live locations | Yes | No | No |
| Extra moving parts | Location index, matcher | None | None |
| Best for | On-demand rides | Small fleets | Shuttles |
Use a real-time matching service for on-demand rides. Manual dispatch is enough for a few vehicles.
Avoiding a new single point of failure
- Location index: partition by city or region, and rebuild from the next round of driver updates if it is lost, since it is only a few seconds old.
- Matcher: run several. Keep ride state in a database so any matcher can continue a ride.
- Connections to drivers: use the same reconnect pattern as chat.
- Regions: one city failing should not take others down.
You accept that locations are slightly stale and that a lost index causes a few seconds of poor matches.
Bottlenecks and failure cases
- Hot cells: a station or stadium puts thousands of drivers and riders in a few index cells. Use smaller cells or split a hot area across several index shards.
- Cell boundaries: the nearest driver can sit in the next cell. Search the rider's cell and its neighbours, then sort by real distance (ideally by travel time, not straight-line distance).
- Stale drivers: a driver whose app lost signal still sits in the index. Check a last-seen time when reading, and drop entries older than a few update intervals.
- Late or duplicate messages: an accept can arrive twice (a retry) or after the offer timed out. Give every offer an id and make the accept idempotent: the conditional update already makes a repeat harmless, and a late accept for a withdrawn offer is rejected.
- Matcher crash mid-offer: the ride is in state
requestedin the database, so another matcher resumes it. Offers it had sent simply time out. - Payment and retries: charging at completion must carry an idempotency key per ride so a retried request cannot charge twice.
Common mistakes
- Storing every location update in the main database. Keep only the latest, in memory.
- Check-then-update for a driver. Two riders can both see the driver as free. Use one atomic update.
- No offer timeout. A silent driver blocks the rider.
- Searching all drivers. Use a spatial index, so you look only nearby.
- Forgetting cancellations. Every state needs an exit and a refund or fee rule.
- One global database for all cities. Partition by region.
How to talk about ride hailing in an interview
- Confirm scale: drivers online, location updates per second, and requests per second in a busy city.
- Draw location updates into a spatial index, a matcher, and a ride state store, with live updates over WebSocket.
- Explain atomic assignment so no driver or ride is double booked, and timeouts for unresponsive drivers.
- Name the trade-off: broadcasting offers matches faster but wastes drivers' attention and needs a single winner.