InfoWok

Design a Ride-Sharing Service: Near, Now, Claimed Once (2026)

A ride-sharing service looks like a map with cars on it and is really three problems: finding nearby drivers among half a million that move every few seconds, ranking them by real travel time, and offering the ride so that no driver is promised to two riders. The prompt, the numbers, the API, and the design.

SK
Sukhveer Kaur
Published October 24, 2026
18 min read
Open in ChatGPT Open in Claude
Ride-sharing service, system design problem 10: a rider request is matched against a live geo index of drivers and offered to one driver, beside the line near, now, claimed onceSoftware Architecture
DESIGN A RIDE-SHARING SERVICE
On this page +

You tap “Request ride”. A few seconds later a driver three minutes away accepts, and a small car icon starts moving toward you on the map. It feels like one tap and one answer.

Behind it, an assumed half a million cars are moving. Each one reports its position every few seconds. In that moment the system must find the drivers near you, work out which one can really reach you soonest by road, ask that driver, and make sure nobody else is promised the same car.

Three words sum up the problem: near, now, claimed once. This lesson builds the system around one rule: index by place, rank by road, claim once.

Intermediate45 min
Before you start

Before You Start#

Remember from the web crawler lesson

How did the crawler make sure a fetcher that crashed did not lose its URL? It handed URLs out with a lease: a claim with a time limit, so unanswered work came back. An offer to a driver is the same idea, with a person on the other end.

Key takeaways
  • Finding nearby drivers is a spatial lookup: cut the map into cells and search the rider’s cell and its neighbours. Cell size trades cells read against drivers examined.
  • Keep only the latest position of each driver in memory, because the live set is small and the writes are the problem.
  • Rank a few candidates by road travel time, not straight-line distance, and match riders in short batches when drivers are scarce.
  • An offer is a fast claim with an expiry, and the accept is a conditional write on a durable ride record, so a driver is never promised to two riders and never stays blocked.
  • A ride is a state machine of stored events, and each transition is applied at most once because it checks the version it expects.
After this lesson you can

Design a ride-sharing service end to end in 45 minutes: state your assumptions, index drivers by location, ingest their updates, rank candidates by road time, offer rides with expiring claims, choose consistency per component, and keep ride state correct. The production side (failures, hot cells, regions, surge, payments, privacy, targets) is in the follow-ups lesson.

Step 1: Pin Down the Problem#

Ask before drawing. Each answer you assume becomes a number:

  • One city or many? (Assume many, but a ride never leaves its city, so cities can be independent shards.)
  • One vehicle type, or several? Pooled or scheduled rides? (Assume one type, none of the others.)
  • Does the system pick the driver, or does the rider browse cars? (Assume the system picks.)
  • How fresh must a driver position be? (Assume a few seconds.)
  • Are pricing, payments and fraud in scope? (Assume not here. The follow-ups lesson covers them.)

Functional requirements:

  1. Drivers go online and share their location.
  2. A rider requests a ride from a pickup point to a destination.
  3. The system finds a nearby driver, offers the ride, and on acceptance links the two.
  4. Both see the ride progress live, until it is completed or cancelled.

Out of scope (say these out loud): pricing and surge, payments, fraud, driver onboarding and ratings, pooled and scheduled rides.

Non-functional requirements, each with a number. These are assumptions you announce:

  • Fast: a rider sees a driver matched within a few seconds when drivers are nearby (assume the median under 10 s).
  • Correct: a driver is never matched to two riders at once, and a rider is never charged for two rides from one tap.
  • Available: a location service hiccup must not stop rides already in progress.
  • Scale: assume 500,000 drivers online at peak across all cities. Step 4 turns that into numbers.

Step 2: Entities, API and Data Model#

text
POST /v1/rides
  {pickup, dropoff, request_id}
  201 new ride
  200 repeat of a request_id
  409 same id, different body
  422 pickup outside area
  429 too many requests
 
GET  /v1/rides/{id}
POST /v1/rides/{id}/cancel
GET  /v1/rides?cursor=...
     (history, paged)
 
Driver connection (held):
  location {lat, lng, heading,
            speed, ts, seq}
POST /v1/offers/{id}/accept
POST /v1/offers/{id}/decline
  200 matched
  409 OFFER_EXPIRED or taken

All calls carry a bearer token. A rider may read only their own rides, and a driver sees the pickup only once an offer is made.

Two fields matter most. request_id is made by the rider’s app, so a retry of the same tap never creates a second ride; a repeat with a different body is a bug, so it fails with 409 (payment APIs such as Stripe’s compare repeat requests with the original for the same reason). expires_at on an offer means a driver who does not answer stops blocking the ride.

Data model#

DataWhereWhy
Live driver position: driver:{id} and cell:{id} setsIn memory, per region, expiring after about 30 sOverwritten every 4 s, rebuilt in seconds
Ride: ride_id, request_id, rider, driver, state, version, pickup, dropoffDurable store, partitioned by city, conditional writesThe record that must never be wrong
Ride events: ride_id, seq, type, timeAppend-only, same storeThe source for both apps and for audits
Offer: offer_id, ride, driver, expires_at, statusMemory with expiry, outcome copied to eventsShort-lived, but the outcome is history
Location trail for tripsStream, then cheap object storageDisputes and analysis, never read on the hot path

Access patterns drive the choices: ride by ride_id, ride by (rider, request_id), the active ride of a driver. Any store with single-row conditional writes works, such as a relational database or DynamoDB with condition expressions, where a write goes ahead only if the condition holds.

Step 3: A Design That Works#

Start with the obvious design. Drivers report their positions into a table. When a rider asks for a ride, the server computes the distance from the rider to every driver, sorts, and takes the nearest few.

It is correct, and for a small town it is fine. It is where many candidates stop.

Predict first

Assume a city has 20,000 drivers online and a rider is in the east of it. How many drivers does the server check, and how many of them could possibly be the answer?

All 20,000 (an assumed city size) are checked, and the answer is almost certainly among the few dozen nearest. Most of the work is wasted on drivers on the other side of the city. A toy test shows the gap: a grid index examined ≈ 338 drivers per request instead of 20,000, and returned the same five drivers every time. The rest of the design is about making “near” cheap.

Step 4: Estimate What Matters#

text
Assume 500,000 drivers online
Update every 4 s:
  500,000 / 4 = 125,000/s
Per update ~100 B on the wire:
  125,000 x 100 B = 12.5 MB/s
  = 100 Mbit/s inbound
Latest position: 100 B each
  500,000 x 100 B = 50 MB
 
Rides: 5M a day / 86,400
  ≈ 58/s, peak ≈ 290/s
Rides in progress (20 min each):
  5M x 1,200 s / 86,400
  ≈ 69,000 on average

Then the neighbourhood:

text
Dense area: 40 drivers/km2
2 km radius: π x 2 x 2
  ≈ 12.6 km2
  ≈ 500 candidates
Driver at 50 km/h
  ≈ 56 m moved per 4 s
Keep every point for a day:
  12.5 MB/s x 86,400
  ≈ 1.1 TB/day

What each number decides#

  • ≈ 125,000 updates a second is the real load, almost all writes. The 100 Mbit/s inbound spreads over many gateways.
  • The live set is only ≈ 50 MB. It fits in memory on one machine, so the problem is write rate and latency, not size.
  • ≈ 290 ride requests a second at peak is small next to the updates. Even 290 × 500 candidates is 145,000 distance checks a second, trivial for memory. Ingest is the bottleneck, not matching.
  • ≈ 1.1 TB a day if every point were kept. That is why the trail keeps only points from trips in progress and expires them, not the whole idle fleet.
  • A driver moves ≈ 56 m between updates. Phone GPS is typically good to about 4.9 m under open sky and worse near buildings, so 56 m of staleness dominates. That is fine for finding candidates, but the final ETA must account for it.
  • All of these are assumptions for the arithmetic. Measure your own.

Step 5: Deep Dives#

Four questions decide this design. Take them in this order unless the interviewer pulls you elsewhere.

How Do You Find Nearby Drivers Fast?#

Concept in use: Hashing keys evenly

Turning a location into a cell id is a hash with a useful property: nearby places get nearby ids. That is what lets a lookup read a small neighbourhood instead of everything.

Scan everything#

Works · Scan every drivercorrect, simplest

Compute the distance to all drivers and sort, as in step 3.

Trade-offCost grows with the number of drivers, not with how many are close. At the assumed 20,000 drivers per city, every request checks all 20,000.

Grid cells#

Scales · A grid: cells, a 3 x 3 block, and ringshandles the load

Cut the map into square cells and file each driver under the cell that holds it. For a request, look in the rider’s cell and the eight cells around it, then widen by a ring if too few drivers are close enough. The search is exact once the fifth-nearest driver is closer than the distance the searched cells guarantee to cover.

A toy test on an assumed 25 × 20 km city with 1 km cells found the five nearest drivers for 2,000 random requests. With 20,000 drivers (40 per km²) the grid examined ≈ 338 per request, not 20,000, and every answer matched. With 1,000 drivers (2 per km²) it examined ≈ 24, and 27% of requests needed a second ring.

Real stores give you this. Redis’s geospatial commands keep points in a sorted set, interleaving latitude and longitude bits into a 52-bit geohash that fits a sorted-set score, and GEOSEARCH finds members in a radius or box, sorted by distance. There is no delete command: use ZREM.

Trade-offStraight-line distance is only a first filter. The grid finds candidates, and a later step ranks them by real travel time.

Hierarchical cells#

Survives · Hierarchical cells, finer where it is densehandles failure and growth

Cities are uneven: a downtown cell holds hundreds of drivers and a suburb cell holds one. Hierarchical indexes let you choose the cell size per area. H3, Uber’s open-source index, uses hexagonal cells where each cell has seven children, changing resolution takes a few bitwise operations, and geographically close places tend to have numerically close indexes. Uber’s H3 post gives the reason for hexagons: a cell has only one distance between its centre and each neighbour’s, where squares have two kinds of neighbour.

The costs are real: the same sources say a child cell is only approximately inside its parent, and every H3 resolution has exactly twelve pentagons among its cells. The H3 page also warns that where exact boundaries matter, the grid should be an optimisation on top of a precise check.

Trade-offMore moving parts: a rule for choosing resolution, and a precise distance check after the cell lookup. Start with a single resolution and add levels only when density varies a lot.

Which index?#

All three common choices turn a point into a number.

IndexCellsStrengthCost
Geohash (Redis GEO)Rectangles from interleaved bits, 52-bit scoreAlready in Redis, one command for radius searchNeighbours across a cell edge can have far-apart numbers, so the store reads several ranges; cell size follows the bit count
S2Squares on six cube faces, four children, 64-bit ids ordered along a Hilbert curveExact nesting; nearby ids are nearby cells, so a database can answer a region with id range scansNeighbour lookup is a little more code; level 13 is about 1.27 km², level 16 about 0.02 km²
H3Hexagons, seven children, resolutions 0 to 15One neighbour distance, simple gridDisk rings, good for per-cell counts such as demandChild cells only approximately inside the parent; pentagons

Recommendation (judgement): for an in-memory index with a rider radius search, H3 or Redis GEO both work; choose H3 if you also want per-cell supply and demand counts, and S2 if the index lives in a sorted key-value store that serves range scans.

What cell size?#

The H3 table lists resolution 8 at an average 0.737 km² and 0.53 km edge, and resolution 9 at 0.105 km² and 0.20 km. A gridDisk of k returns every cell within k hops, which is 3k(k+1)+1 cells.

text
Target: 2 km radius = 12.6 km2
Neighbour spacing = 1.73 x edge
 
Res 8: spacing ≈ 0.92 km
  k = 3 -> 37 cells x 0.737
  = 27.3 km2  -> 1,090 drivers
Res 9: spacing ≈ 0.35 km
  k = 6 -> 127 cells x 0.105
  = 13.4 km2  ->   535 drivers
(at 40 drivers/km2)

Using the H3 areas, resolution 8 does 37 lookups and examines about 1,090 drivers, and resolution 9 does 127 lookups and about 535. Measure which is cheaper (judgement). Downtown, at an assumed 400 drivers/km², a resolution 8 cell holds ≈ 295 drivers, so dense areas want a finer one. The ring counts are rough: a precise distance check makes the result exact, and the search widens if fewer than five pass.

How Do You Ingest 125,000 Updates a Second?#

Concept in use: Design a chat system

Drivers hold a connection to a gateway, as chat clients do. The updates arrive through it, and only the latest one matters.

Write every update#

Works · Write every update to a databasecorrect, simplest

Each update is a row insert or update in a table of drivers.

Trade-offAt ≈ 125,000 writes a second on rows that are overwritten every few seconds, the database does the most work on the data that matters least.

In memory#

Scales · Update an in-memory index in placehandles the load

Keep the live positions in memory, in the grid or in a geo sorted set, and update a driver’s entry in place. When a driver crosses into a new cell, remove them from the old cell and add them to the new one. At about 50 MB the live set is tiny, so one process per region holds it all.

Make the write safe to repeat and to reorder. Each update carries a per-driver sequence number seq, and the index ignores one that is not newer than the stored one, so a delayed packet never moves a car backwards. Raw phone GPS is noisy, so snap points to the road network with map matching, which Valhalla provides.

Trade-offMemory is not durable. If the process restarts the index is empty until drivers report again, which takes seconds at a 4 second interval. That is acceptable for locations, but not for ride state.

By region#

Survives · Regions, adaptive rates and a separate historyhandles failure and growth

Give each region its own location service, with one writer per region so updates never race. This is sharding by place. Let the app adapt its rate: faster when a driver is close to a pickup, slower when idle, which is a judgement call. Mark a driver offline when no update has arrived for a set time, such as 30 seconds. Send history to a separate stream for analysis so it never slows the live index.

Trade-offRiders near a region border need both regions’ drivers, so the search reads two shards. Pick region borders along places few trips cross, such as rivers and city limits.

How Do You Match Without Double-Booking?#

Concept in use: Design a web crawler

A lease: work handed out with a time limit, so a silent worker does not hold it forever. An offer to a driver works the same way.

Nearest wins#

Works · Take the nearest and tell the drivercorrect, simplest

Pick the nearest driver on the map and send the ride.

Trade-offTwo riders in the same minute can pick the same driver. And the nearest on the map can be far by road, for example across a river.

Rank, then claim#

Scales · Rank by road time, then claim the driverhandles the load

Take the ≈ 5 best candidates from the grid and rank them by travel time. OSRM has a Table service that returns durations between all pairs of coordinates, and Valhalla offers a time and distance matrix. One call ranks all candidates.

Then claim the driver atomically. In Redis, SET with NX only sets a key that does not exist, and PX gives it an expiry in milliseconds. One rider’s claim holds the driver, a second rider’s attempt fails, and if the driver never answers the claim expires by itself.

Trade-offAn offer holds the driver for its whole window. At an assumed 15 seconds per driver, three declines cost up to 45 seconds, so keep the window short.

Sequence and batches#

Survives · Offer in sequence, widen, and match in batcheshandles failure and growth

If the first driver declines or the claim expires, release it and offer to the next one. If candidates run out, widen the search ring. Make every step safe to repeat by keying it on the rider’s request_id. When one step must update several keys together, such as the driver’s claim and the ride’s state, use an atomic script: Redis guarantees a script’s atomic execution.

When riders and drivers are both scarce, matching one rider at a time is greedy. Uber describes the alternative: evaluate nearby riders and drivers in one batch in the seconds after a request, aiming to reduce the average wait for everyone, not just the closest pair. Pairing riders to drivers so total cost is smallest is an assignment problem, and solvers such as the Hungarian algorithm handle a batch of hundreds quickly (judgement on size).

Trade-offBatching adds a small delay to every rider to improve the average. Test it with real demand before adopting it.

Why batching wins#

Assume two riders and two drivers with pickup ETAs in minutes (made-up numbers):

text
        D1   D2
R1       2    3
R2       3    9
 
Greedy, R1 first:
  R1-D1 (2) then R2-D2 (9) = 11
Batch, best pairing:
  R1-D2 (3) + R2-D1 (3)   =  6

The batch gives an average wait of 3 minutes against 5.5. If a batch window is 2 s and requests arrive at the peak of 290/s, a batch holds about 580 riders, a real assignment problem. Because a wide batch costs rider wait, shrink the window when supply is plentiful and widen it when it is scarce (judgement).

What Do You Pick Under a Partition?#

The claim must not double-book, even when a node fails, so state a stance per component rather than a slogan. CAP and PACELC give the words.

ComponentDuring a partitionOtherwiseWhy
Live locationsStay available, serve stale positionsLow latencyA 30 s old car is a small error
Offer claimRefuse to offer rather than risk twoA few ms more for the checkA double booking costs a rider and a driver
Ride recordConsistent: one owner per ride, reject on doubtConditional write per transitionThe record is the truth for money and trust
Rider and driver pushesAvailable, may be lateLow latencyThe app can fetch the state again

The claim is a fast filter, not the final word#

Redis uses asynchronous replication, so acknowledged writes can be lost during a failover, and WAIT does not turn it into a strongly consistent system. After a failover a claim can vanish and a second rider could claim the same driver.

So the accept is a conditional write on the durable ride record, such as “set driver = D and state = matched only if state is offered and the offer id is X”. Only one write passes, and the loser gets a 409. A stale holder is also why locks use fencing tokens: each offer carries an increasing number, and the store rejects anything older.

In PACELC terms this is PC/EC for claims and ride state and PA/EL for locations (judgement). Idempotent accept and decline follow from the same write: a double tap finds the ride already matched to that driver and returns 200 again; an accept after the offer expired fails with 409 and the app shows “no longer available”. When the 15 s timer and the accept race, both are conditional writes on the same version, so exactly one wins.

How Do You Keep Ride State Correct?#

Concept in use: Design a notification service

Store the record first, then tell people. Both apps learn about a ride the same way: from stored events, with a nudge on top.

Polling#

Works · Both apps poll the servercorrect, simplest

The rider’s and driver’s apps ask for the ride’s state every few seconds.

Trade-offDelays of seconds and many wasted requests. It works, but a ride feels laggy.

Stored events#

Scales · A state machine with stored events and held connectionshandles the load

A ride moves through fixed states: requested, offered, matched, driver arriving, in progress, completed, with cancellation allowed before the ride starts. Store every transition as an event first, then push it to both apps over their held connections. If an app drops and reconnects, it resumes from its last event number, as in the chat lesson. While the app is in the background, a mobile push nudge wakes it, as in the notification lesson.

Trade-offTwo writers can race, for example a cancel and an accept at the same moment. Let one place decide, such as a single owner per ride, and reject any transition that is not legal from the current state.

Idempotent transitions#

Survives · Idempotent transitions and regional shardshandles failure and growth

Key every transition on a request or event id, so a retry that arrives twice is idempotent and changes nothing the second time. Every update says “from version v to v+1”, so a repeat fails the check and changes nothing. That is how at-least-once delivery becomes an effect that happens once. Shard ride state by city, since rides rarely cross cities.

Trade-offA regional shard can be overloaded while the rest sit idle, and a city outage stops its rides. The follow-ups lesson covers hot cells and failover.
FromToTriggerRule
requestedofferedMatcher picks a driverNeeds the driver claim
offeredofferedDecline or timeoutNext offer, version +1
offeredmatchedDriver acceptsConditional on offer id and version
matcheddriver arrivingDriver starts navigationDriver only
arrivingin progressPickup confirmedDriver or rider app
in progresscompletedDrop-off confirmedTriggers payment
requested to arrivingcancelledRider or driver cancelsFee rules are product choices
A rule to remember

Index by place, rank by road, claim once. A grid tells you who is near, a routing engine tells you who is soonest, and a claim backed by a conditional write makes sure only one rider gets them.

The Final Design in One Picture#

Follow one ride:

  1. Drivers send their position every few seconds. The regional location service updates its in-memory grid.
  2. A rider taps request. The app sends a request_id with the pickup and destination.
  3. The matching service reads the cells around the pickup and takes the closest candidates.
  4. It asks the routing service for travel times from those candidates to the pickup, and ranks them, in a batch when drivers are scarce.
  5. It claims the best driver with an expiring key and sends an offer. If the driver declines or the claim expires, it releases the claim and offers to the next one.
  6. On acceptance a conditional write stores the ride as matched, and both apps are notified. Later transitions are stored first, then announced.

What Each Level Should Show#

LevelExpected in the room
Mid-levelA driver table, a distance query, a notification to the driver. May need a hint about scale and spatial indexes.
SeniorA grid or geohash index, in-memory live locations, ETA ranking by road, an expiring claim on the driver, a ride state machine, an idempotent request.
Staff+Cell size arithmetic, batch matching, a per-component consistency stance with fencing, regional shards, hot cells, and what to build first.

Red Flags and Green Flags#

✗ Red flag

“Store driver locations in a database and query the nearest ones.” Nothing about the assumed 125,000 writes a second, road distance, or two riders getting the same driver.

✓ Green flag

“The live positions are only tens of megabytes, so I’ll keep them in memory per region in a grid. I’ll rank the few nearest by road time from a routing service, then claim the driver with an expiring key and make the accept a conditional write, so one rider gets them even after a failover.”

✗ Red flag

“Redis SET NX makes sure nobody is double-booked.” It ignores asynchronous replication, a paused holder, and a claim that expires while the driver is tapping accept.

✓ Green flag

“The claim is a fast filter. The truth is a conditional write on the ride record, with a version, so a late accept gets a 409.”

The most common way to lose this round

Spending the time on the map and the “nearest driver” query and never reaching the double booking. The interviewer is waiting for the write load, the claim and what happens when a driver does not answer.

Try It: Choose the Cell Size#

Exercise · Resolution 8 or 9?10 min

A downtown zone has 400 drivers per km², a suburb has 10 per km². Using the H3 areas above (resolution 8 is 0.737 km², resolution 9 is 0.105 km²):

  1. How many drivers does one cell hold downtown and in the suburb, at each resolution?
  2. Which resolution would you use for each zone, and why?
  3. What happens in the suburb if a rider’s 3-ring search finds only 2 drivers?
Hint

Multiply density by cell area. Then ask which is worse: a cell with hundreds of drivers to examine, or a cell with none.

One reasonable answer
text
Downtown 400/km2:
  res 8: 400 x 0.737 ≈ 295
  res 9: 400 x 0.105 ≈  42
Suburb 10/km2:
  res 8: 10 x 0.737 ≈ 7
  res 9: 10 x 0.105 ≈ 1

Downtown, use resolution 9 (or finer): 42 drivers per cell is cheap to scan, while 295 per cell wastes work. In the suburb, use resolution 8: at resolution 9 most cells are empty, so a search needs many rings.

If a 3-ring search finds only 2 of the 5 wanted drivers, widen to the next ring and repeat, as the toy test did in 27% of requests at 2 drivers per km². Keep a maximum radius, after which the rider is told no cars are available and is offered a retry.

In an interview

Expect the follow-ups in this order: “How do you find drivers near a rider?”, “How do you handle 125,000 location updates a second?”, “What if two riders request the same driver?”, and “What happens when a driver does not answer?” Each one maps to a deep dive above. Close each answer with one trade-off and one figure. Then they will ask what breaks, how a stadium crowd, surge pricing, payments and location privacy work: that is the follow-ups lesson.

Check Yourself#

Check yourself5 questions · answer, then open
1.Why does a grid beat scanning every driver, and when does it need a second ring?

It looks at the rider’s cell and its neighbours, a few hundred drivers instead of all of them. It needs a second ring when the area is sparse and fewer than the wanted number of drivers are close enough, which happened in 27% of requests at 2 drivers per square kilometre in the toy test.

2.Why keep driver locations in memory, and what do you lose on a restart?

The live set is tiny but overwritten every few seconds, so a database wastes effort. A restart empties the index until drivers report again, which takes seconds. That is fine for locations, but not for ride state.

3.Two riders choose the same driver in the same second, and the Redis primary fails over right after. What stops a double booking?

The Redis claim stops the first collision, but replication is asynchronous, so the new primary may not have it. The accept is a conditional write on the durable ride record, which succeeds for one offer only. The other rider gets a 409 and moves to the next candidate.

4.A rider's app retries the request after a timeout. How do you avoid two rides?

The app sends the same request_id each time. The server stores the ride keyed by it, so a repeat finds the existing ride and returns it instead of creating another. The same id with a different body fails with 409.

5.When does batch matching beat nearest-first, and what does it cost?

When drivers are scarce: the first rider may take the driver a second rider needed (11 minutes against 6 in the example). It costs every rider a short wait, so shrink the window when supply is plentiful.

Learning tip

Tomorrow, say the rule aloud, then trace one ride from tap to match in two minutes. For each step name what could fail (a restart, a decline, a double tap) and which part handles it.

In One Picture#

text
Requirements
  locate, request, match, track
Scale
  125,000 updates/s · 50 MB live
Near
  cells sized to density
Soonest
  road ETA, batch when scarce
Claim
  fast key + conditional write
State
  versioned events, one owner
Rule
  index by place, rank by
  road, claim once

Conclusion#

A ride-sharing service is a live map plus a careful handshake. Keep the latest position of every driver in a spatial index in memory, rank a few candidates by real travel time, and claim the winner with an expiring key backed by a conditional write so only one rider gets them. Store every ride transition before announcing it, and shard by city.

The same ideas, cheap lookups for “near”, leases for work that can go quiet, and stored events for state, run through most of the problems in this module.

Which would you defend first in an interview: the grid, the ETA ranking or the claim? Tell us in the comments.

Where to go from here

Frequently asked questions

How do you find the nearest drivers to a rider efficiently? +
Use a spatial index instead of scanning every driver. The simplest is a grid: split the map into cells, file each driver under its cell, and search the rider's cell and the cells around it, widening the ring if too few drivers are close enough. Redis stores geo points in a sorted set using a geohash, and Uber's H3 index uses hierarchical hexagonal cells.
What is the difference between geohash, S2 and H3? +
All three turn a location into a cell id so that nearby places share or neighbour ids. A geohash interleaves latitude and longitude bits into one integer, which Redis keeps as a sorted-set score. S2 projects the sphere onto six cube faces and orders square cells along a Hilbert curve, with four children per cell. H3 uses hexagons with seven children per cell, so every neighbour of a cell is at the same distance, and the containment of children is approximate.
How do you keep two riders from getting the same driver? +
Make the offer an atomic claim with an expiry, and make the ride record the final judge. In Redis, SET with NX only succeeds if the key does not exist and PX sets a time limit. Because Redis replication is asynchronous, a failover can lose a claim, so the accept itself is a conditional write on a durable ride record that succeeds only if the ride is still in the offered state for that offer.
Should a ride-sharing service match each rider to the nearest driver? +
Nearest-first is simple and fast but greedy: the first rider can take the driver a second rider needed. Uber describes batching, evaluating nearby riders and drivers together for a few seconds and pairing them to reduce average wait for everyone. The cost is a small delay for every rider, so the batch window is a tuned number, not a constant.
How often should drivers send their location? +
It is a trade-off between freshness, battery and write load. At 500,000 drivers every 4 seconds is 125,000 updates a second, and a car at 50 km/h moves about 56 metres between updates. Many designs send faster near a pickup and slower when idle, and keep only the latest position in the live index.

References

  1. Redis documentation: GEOADD (geohash in a sorted set)
  2. Redis documentation: GEOSEARCH
  3. H3 documentation: Indexing (hierarchical hexagonal cells)
  4. H3 documentation: Table of cell areas and edge lengths per resolution
  5. H3 documentation: Traversal functions (gridDisk)
  6. Uber Engineering: H3, Uber's hexagonal hierarchical spatial index
  7. S2 Geometry: the cell hierarchy (Hilbert curve, 64-bit cell ids)
  8. S2 Geometry: cell statistics per level
  9. Uber: how batched matching works
  10. OSRM: Open Source Routing Machine (Nearest, Route, Table)
  11. Valhalla: open source routing engine (matrix, map matching)
  12. GPS.gov: GPS accuracy (smartphones, open sky versus buildings)
  13. Redis documentation: SET (NX and PX options)
  14. Redis documentation: Scripting (atomic execution)
  15. Redis documentation: Replication (asynchronous, acknowledged writes can be lost)
  16. Stripe API: idempotent requests (same key, same body)
  17. Amazon DynamoDB: condition expressions (conditional writes)
  18. Martin Kleppmann: How to do distributed locking (fencing tokens)
Written by
Sukhveer Kaur
Sukhveer KaurSoftware Developer & AI Engineer

Sukhveer is a software developer specialising in AI systems and backend engineering. She has hands-on experience designing agentic AI applications, working with large language model pipelines, autonomous agent frameworks, and cloud-native services in Java and Python. At InfoWok, she bridges the gap between cutting-edge AI research and practical implementation — helping developers understand and apply emerging technologies through clear, experience-backed writing.

New AI engineering guides, the day they ship

Real Python, production depth. No digest spam.

Comments