Finding forty is cheap, choosing one is the product
The spatial index already did its job. It turned tens of thousands of drivers into forty candidates in a few milliseconds, and it did so using straight-line proximity because that is the only thing a geospatial key can express. Now the expensive question starts, and it is a different kind of question.
The reflex answer is nearest. It is wrong often enough to matter. A driver eight hundred metres away on the far side of a railway line, facing the wrong direction on a dual carriageway, may be six minutes from the rider. A driver two kilometres away on the same road heading towards them may be three. What the rider cares about is time to pickup, and time to pickup is a routing computation, not a distance.
So the pipeline has two stages with different budgets. Candidate generation is cheap and approximate, and it can afford to be run on every request. Ranking is expensive and it runs over dozens, not thousands. Send forty candidates to a routing service, get forty estimated pickup times back, and you have paid for accuracy exactly where it changes the decision. A strong candidate says the split out loud: "The index finds, the router ranks, and I only ever ask the router about a few dozen cars."
The clock the rider is watching
The budget is set by a human staring at a screen. A few seconds of spinner is normal, ten feels broken, and thirty means they open a competitor. Everything below has to fit inside that.
Break it down. Candidate lookup is milliseconds. Batch routing for forty candidates is a few hundred milliseconds if you call one service once rather than forty times. Ranking is arithmetic. That leaves the real consumer of the budget: waiting for a driver to accept.
An offer is not a decision, it is a proposal to a person who may be parking, talking, or ignoring their phone. So the offer carries a timeout of somewhere around ten to twenty seconds, and after it lapses you move to the next candidate. Two declines in sequence and you are already past the rider's patience, which is why the rider-facing experience has to be decoupled from the offer loop: the rider sees "finding your driver" while the system works through candidates, and only learns the outcome once.
An outstanding offer is a lock, and this is where correctness lives
While a driver is considering an offer they must not be offered to anyone else. Two riders both offered the same car is the same class of bug as selling one seat twice, and it produces the same visible failure: one rider watches a driver they were promised drive somewhere else.
So an offer writes a soft lock on the driver, with an owner and an expiry, claimed by a conditional operation that either succeeds or tells you the driver is taken. Not a read followed by a write. The expiry is what makes it safe against a dispatcher crashing mid-offer, because the lock lapses on its own rather than stranding a driver who is available and invisible.
The states a driver moves through are worth naming: available, offered, assigned, on trip, and then available again. Only available drivers enter candidate generation. That means the index has to reflect the lock quickly, or your candidate list keeps proposing cars that are already committed, and every dispatch wastes an offer discovering it.
Greedy per request, or a short batching window
Here is the trade-off an interviewer is usually fishing for. Dispatching each request the moment it arrives is greedy: give this rider their best driver. It is simple, it is fast, and it is measurably worse for the market as a whole.
Consider the smallest case that shows it. Two riders, A and B, and two drivers. Driver one is two minutes from A and three minutes from B. Driver two is nine minutes from A and twenty from B, because B is out past a bridge. Handle A first and greedily give them driver one: A waits two minutes, B is left with driver two and waits twenty, so the total is twenty-two. Pair them the other way and A waits nine while B waits three, for a total of twelve. The greedy choice was optimal for the rider who happened to ask first and cost the pair ten minutes.
Batching makes that visible. Hold requests for a few seconds, collect the drivers and riders in the window, and solve it as an assignment problem over the resulting cost matrix rather than one row at a time. With dozens of riders and dozens of drivers this is small enough to solve exactly, and it consistently beats greedy on average wait.
What batching costs is honesty about latency. Every rider now waits for the window before anything happens, and a rider at three in the morning with one driver nearby waits for no benefit at all. The usual resolution is to make the window adaptive: near zero when supply is plentiful relative to demand, a few seconds when the market is dense enough that pairing choices exist. Whether riders prefer it is a question for an experiment, not for an architecture diagram.
What else is in the cost function, and what should not be
Pickup time is the base, and a few other terms are defensible. A driver approaching the end of their shift should not be sent on a long airport run. A vehicle class or accessibility requirement is a hard filter, not a weight. Trip length balance across drivers keeps the supply side willing to work. Direction of travel matters, because a driver already pointed the right way avoids a turn that adds minutes.
Two things are worth being careful about. Ranking by acceptance rate seems reasonable and quietly builds a system where declining once costs a driver income, which is a policy decision disguised as a scoring tweak. And optimising for revenue per assignment rather than for wait time produces choices that are hard to explain to either side of the marketplace. Whatever the terms, record them with the assignment. When a driver asks why they did not get a trip they were nearest to, the answer needs to exist.
The failure that looks like nothing is wrong
The one to walk through is the cascade of silent declines. Supply is thin, the top candidate ignores the offer, the second is driving and does not look, the third declines. Ninety seconds have passed, the rider is still watching a spinner, and no component has logged an error. Dispatch is working exactly as designed and the outcome is a rider who leaves.
Three things address it. Widen the radius as candidates are exhausted, so the search relaxes over time instead of failing at the original bound. Cap total dispatch time and tell the rider the truth when it runs out, because "no cars available right now" is a better product than an endless spinner. And send offers to two or three drivers at once when supply is thin, accepting that a race between them wastes a driver's tap, because a wasted tap is cheaper than a lost trip. That last one is a deliberate exchange of driver experience for rider conversion, and saying so is better than pretending it is free.
Candidate generation is a spatial problem and assignment is an economics problem, and the second one is bounded not by compute but by how long a person will look at a spinner before deciding your product does not work.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).