Skip to content

Ride Hailing

advanced · 40 min · Last reviewed 31 Oct 2026 · by TheLearningLoop Team

Key takeaways

  • Locations are a fast, approximate firehose kept in an in-memory spatial index. Trip state is small and strict, kept in a database.
  • Assign a driver with one atomic conditional update, so a ride never has two drivers and a driver never has two rides.
  • Offers need timeouts. Offering to several drivers is faster but more wasteful than offering one at a time.

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 step

Ride requested

Riderrequested

waiting for a driver

D1free

0.4 km away

D2free

0.9 km away

D3free

1.3 km away

D4free

2.2 km away

Initial 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

StrategyHow it worksGood whenWeakness
Nearest driver, one at a timeOffer to the closest driver, wait, then try the nextFew riders and plenty of driversA slow or absent driver delays the rider
Broadcast to severalOffer to a few nearby drivers, the first to accept winsFast matching mattersDrivers see offers they will not get, and the server must pick one winner
Batch matchingCollect requests for a few seconds and assign globallyBusy areasAdds a short wait but gives better overall pickups
Pre-assigned zonesDrivers wait in a queue per areaAirports and stationsNot 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

  1. The rider requests a ride, and the service creates a ride record in state requested.
  2. It queries the location index for free drivers within a radius, sorted by distance.
  3. It sends an offer to a driver through their open connection or a push message, and sets a timeout.
  4. When a driver accepts, the service performs one atomic update: change the ride to matched with this driver only if it is still requested, 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.
  5. If the offer times out or is declined, it moves to the next driver.
  6. 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

ConcernReal-time matching serviceManual dispatchFixed schedules
Matches in secondsYesSlowNo
Handles thousands of driversYesNoYes
Prevents double bookingYes, atomic updateBy handYes
Uses live locationsYesNoNo
Extra moving partsLocation index, matcherNoneNone
Best forOn-demand ridesSmall fleetsShuttles

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 requested in 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.

Where you meet ride-hailing patterns in real systems

Taxi apps, food delivery, couriers and field-service dispatch all match moving supply to demand. Here are three drawn out, the first with configuration you can run, then more examples.

Example 1: Finding nearby drivers and assigning one

Redis keeps the latest position of each driver in a geospatial index. A relational table holds the ride, and one conditional update makes sure only one driver gets it. Open the example below to see the picture and the commands.

Config: a location index and atomic assignmentSetup 1 table and 4 commandsCode no code changes
Ride requestedstate: requestedFind nearbylocation indexOffertimeout if silentAssign onceconditional update
The matcher asks the location index for nearby free drivers, offers the ride, and assigns it with one conditional update so only the first acceptance wins.
# Redis geospatial index: the latest position of each free driver. Longitude comes before latitude.
# Replace the coordinates and ids with your own.

# Drivers report their location every few seconds. Re-adding the same member updates its position.
GEOADD drivers:city1 77.5946 12.9716 driver:1 77.6010 12.9750 driver:2 77.6200 12.9800 driver:3

# The matcher finds free drivers within 3 km of the rider, nearest first, with their distance.
GEOSEARCH drivers:city1 FROMLONLAT 77.5950 12.9720 BYRADIUS 3 km ASC WITHDIST COUNT 5

# When a driver is assigned, remove them from the free index so no other rider is offered them.
ZREM drivers:city1 driver:1

# A driver who stops reporting should disappear. In practice also keep a short-lived "last seen" key per driver.
SET driver:2:lastseen 1700000000 EX 15

The Redis commands were run against a real Redis 7 server: adding drivers, a radius search returning them nearest first with distances, and removing one. The SQL was run against a real PostgreSQL 16 server: the first assignment changed one row and the second changed none. Names and coordinates are examples.

This example guards the ride only. In a full design the driver needs the same treatment, so a driver cannot accept two rides at once: change the driver from free to busy with a conditional update (or a short lock) before updating the ride, and undo that if the ride update changes no row. The ride row is the source of truth, because the Redis index and the PostgreSQL table cannot change in one transaction.

A note on the last Redis command above: setting a key with EX 15 expires that key only. It does not remove the driver from the geospatial index, because the members of a GEO index live inside one sorted set and cannot expire one by one. So the matcher must read the last-seen key (or a stored timestamp) and skip drivers whose key is gone, and a periodic sweep should remove them from the index with ZREM.

Example 2: The trip as a state machine

Each ride moves through a small set of states, and only some moves are allowed.

RequestedwaitingMatcheddriver acceptedStartedrider on boardCompletedpay
A ride moves forward one state at a time. Cancelling is possible from the early states, and each change is a conditional update so it cannot happen twice.

The diagram and the table above are simplified. A fuller model also has an arrived state between matched and started, plus cancelled and no-show exits, each with its own fee rule.

Example 3: Updates and reads on different paths

Location updates go to the fast index. Trip changes go to the database. The rider sees the driver move through a live connection.

Driver appLocation indexMatcherRide databaseRider app
Driver apps send locations to an in-memory index. The matcher reads that index and writes trip changes to the database. The rider's app receives live updates.

More real-world scenarios

The scenarios below are illustrative descriptions of the pattern, not claims about any named company.

4. Food delivery

Orders are matched to nearby couriers, often several orders per courier, so batching matters more than the nearest courier.

5. Parcel couriers

Pickups are assigned to the nearest van with capacity, and routes are re-planned as new jobs arrive.

6. Surge pricing

When requests in an area exceed free drivers, the price rises. Counts per area are computed from the location index every few seconds.

7. Scooter and bike rental

The nearest available vehicle is shown on a map. Reserving it is the same conditional update so two people cannot unlock one scooter.

8. Field service dispatch

Technicians are matched to jobs by skill, location and schedule, with the job as a state machine.

9. Ride sharing

Several riders heading the same way share a car. The matcher also checks the detour each new rider causes.

10. Emergency vehicles

Dispatch picks the nearest suitable unit and treats a lost message as serious, so offers are retried and confirmed.

Tools and managed services

KindNameNotes
In-memory indexRedis (GEO commands)Latest positions and radius search
Spatial databasePostGIS for PostgreSQLGeographic queries and storage
ConnectionsWebSocket servers, push notification servicesOffers and live tracking
StreamingApache KafkaLocation history and events for analytics
MapsMap and routing providersDistance, routes and arrival times

The takeaway

Whenever a design matches moving supply to demand, keep locations in a fast index you can search by distance, and assign each match with one atomic conditional update.

Interview questions

Try answering out loud before you reveal hints. Interviewers care about how you reason, not just the final answer.

  1. 1.

    How do you find the drivers closest to a rider among hundreds of thousands?

    medium
  2. 2.

    Two riders request at the same moment and the same driver is nearest to both. How do you prevent double booking?

    hard
  3. 3.

    The nearest driver never answers the offer. What do you do?

    easy
  4. 4.

    Drivers send a location every 4 seconds. Where do you store it?

    medium
  5. 5.

    How would you scale this to many cities?

    medium
  6. 6.

    Estimate the load for one busy city with 100,000 drivers online.

    medium
  7. 7.

    A driver loses signal but still appears in the index. How do you avoid offering rides to them?

    medium

Finished this lesson?

Progress is saved on this device. Create a free account to keep it everywhere.

Topics:Ride hailingUberMatchingLocation serviceDispatchState machineCase study

↑ Back to top

Next lesson