Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

System Design

Design a Ride-Matching Engine

Step 1 / 9

Learn system design by building the matching and dispatch engine at the heart of a ride-hailing platform — the specific subsystem that decides which driver gets which rider, not the trip lifecycle…

The numbers to beatsecondstypical batch windowassignment problemsolved per windowlower total ETAthan greedy, on average

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What is a matching engine, specifically?

Not the whole ride-hailing product — just the one hard question buried inside it: given a rider who just requested a ride and a hundred available drivers scattered across the city, WHICH driver should be assigned? Pick wrong and you've either sent a driver 20 minutes away when a 3-minute driver existed, or you've locally optimized one match while leaving the rest of the marketplace worse off.

Riderrequests a rideDriverpings location
New in this step: Rider, Driver.

We'll build the matching pipeline specifically: how "nearby" is actually measured, why matching one rider at a time is provably worse than matching a whole window together, how surge pricing balances a two-sided marketplace, and how fairness keeps the system healthy for drivers, not just efficient for any one rider.

What the new pieces do

Riderclient
Wants a driver assigned fast, and wants that driver to actually be close by real travel time — not just close on a map.
Driverclient
Streams a location update every few seconds while online and available, and expects fair, not-too-sparse ride offers.

Step 1 · The baseline

Nearest driver, by straight-line distance

Simplest matching: find the geographically closest available driver (straight-line distance on a map) and assign them immediately, one rider at a time. What goes wrong?

Dispatch APIrequests + pingsDriver Geo Indexlive locations
New in this step: Dispatch API, Driver Geo Index.

Matching by straight-line distance, one rider at a time, immediately. What's the core failure?

  1. Closest ON A MAP and closest by actual travel time are very different things the moment a river, highway, or one-way street sits between two points — "closest" needs a better definition.

  2. Two separate flaws stack here: the DISTANCE METRIC is wrong (straight-line ≠ real travel time across a road network with barriers), and the ALGORITHM is short-sighted (assigning the first rider to their nearest driver might use up a driver who was actually the BEST match for a rider arriving a second later, while a different driver would have served the first rider almost as well).

  3. Driver acceptance behavior is a real factor in production systems, but it's not the CORE algorithmic flaw here — the flaw exists even assuming every driver accepts every offer.

Two separate problems: the distance metric is wrong (straight-line ignores rivers, highways, one-way streets — real travel time can differ wildly from map distance), and the algorithm is short-sighted (matching greedily, one rider at a time, can lock in an assignment that's locally fine but globally worse than considering multiple pending riders and drivers together).

Why this piece earns its place

The baseline also bakes in an assumption nobody states out loud: that the drivers worth considering are the ones sitting idle at the instant the request lands. That holds in a quiet suburb and breaks exactly where it matters most — downtown at peak, where almost every driver is mid-trip and the idle pool is thin or empty. The genuinely best answer to a request arriving now is often a driver a block away who is about to drop off, not the one free car on the far side of a river. A matcher that cannot see them either offers a bad driver or tells the rider nobody is available while the street outside is full of cars. So let a driver enter the candidate pool before they are free: carry their predicted drop-off point and the time they will be free, and cost them as available-from-then rather than unavailable-now. It buys nothing when supply is loose, and it is most of the difference during a rush — which is precisely when every other part of this design is also under strain.

What the new pieces do

Dispatch APIgateway
Ingests ride requests and driver location pings, and is the front door to the whole matching pipeline.
Driver Geo Indexcache
A fast, frequently-updated spatial index of every available driver's current location — the thing matching queries against.

Step 2 · Real ETA, not map distance

Route, don't just measure

Fix the metric: use actual predicted travel time instead of straight-line distance.

Dispatch APIrequests + pingsDriver Geo Indexlive locationsETA / Routingreal travel time
New in this step: ETA / Routing.

Introduce an ETA / Routing service that computes real road-network travel time between a candidate driver and the rider — accounting for actual streets, current traffic conditions, and turn restrictions. This becomes the true cost function for matching: rank candidate drivers by predicted pickup ETA, not by distance on a map.

Why this piece earns its place

Real routing is the correct cost and also the most expensive thing on this page. A straight-line lookup against the geo index is nearly free; a road-network route is not, and one request against a hundred available drivers means a hundred of them — worse in Step 4, where a whole window of riders and drivers needs a cost matrix, not a column. So straight-line distance does not disappear when we stop ranking with it; it gets demoted to the candidate filter. Pull a handful of plausible drivers cheaply out of the index, then spend real routing only on those. The radius of that filter is now a knob, and both directions cost something: too tight in a sparse area and you route three drivers, none of them good, so the rider waits for a bad match; too wide downtown and you are paying to route cars that were never going to win the assignment. Tie it to local driver density rather than pinning one number citywide, and widen it automatically when a region returns too few candidates to choose between.

What the new pieces do

ETA / Routingservice
Computes actual road-network travel time between a candidate driver and a rider — the real cost function, not straight-line distance.

Step 3 · Surge pricing

When demand outstrips nearby supply

A concert lets out: 500 ride requests appear in six square blocks with 40 available drivers. Matching can't manufacture drivers that don't exist. What should the system do?

Dispatch APIETA / RoutingSurge Pricing
New in this step: Surge Pricing. · swipe to pan the diagram

Demand vastly exceeds nearby driver supply. What can the system actually do about it?

  1. Doing nothing beyond matching leaves both sides of the marketplace unaddressed: riders have no signal about the wait, and there's no incentive pulling additional drivers toward the imbalance.

  2. Surge is a two-sided lever: it moderates demand (price-sensitive riders wait it out) AND grows supply (higher fares pull drivers who'd otherwise be elsewhere or offline). It's a real-time price signal doing double duty in a two-sided marketplace with a genuine, temporary supply shortage.

  3. Hiding the reason for a long wait removes the price signal that would otherwise pull in more supply, and gives riders no information to make an informed choice (wait, or find another way).

Introduce surge pricing: when a geographic area's demand meaningfully exceeds nearby supply, raise the price for that area in near-real-time. This is a genuine market-clearing mechanism, not just a revenue lever — it rations demand toward those who value the ride most right now, and pulls additional driver supply toward the shortage.

Why this piece earns its place

Surge is a feedback loop, and feedback loops oscillate unless you damp them. Price rises because supply is short, drivers reposition toward the money, the shortage clears, the price falls — and the drivers who crossed town arrive into an ordinary fare and learn not to chase it next time. Two things keep that honest. Quote the price when the rider requests and honour it, so nobody is repriced mid-decision; and rate-limit how fast the multiplier can move in either direction, so it describes a real imbalance rather than a momentary blip in arrivals. The other knob is the zone itself. Draw it too coarse and you surge riders several blocks from the concert who caused none of it. Draw it too fine and the boundary becomes the game — riders walk one street over to dodge it, drivers idle on the profitable side of a line rather than where riders are. Neither is a pricing bug you patch later; both follow directly from where the box was drawn, which makes the box a design decision, not a configuration detail.

What the new pieces do

Surge Pricingservice
Raises price when demand outstrips nearby supply — both rationing demand and pulling more drivers online toward the imbalance.

Step 4 · Batch, don't match greedily

Solve the whole board, not one move at a time

Even with correct ETAs, matching riders the instant each request arrives — one at a time, in whatever order they happen to land — can produce a worse OVERALL assignment than considering several pending riders and drivers together. How do you match better than greedy, without making every rider wait forever for a "perfect" global solve?

DriverDispatch APIETA / RoutingBatch Matcher
New in this step: Batch Matcher. · swipe to pan the diagram

Greedy one-at-a-time matching is provably suboptimal. What's the practical fix?

  1. Riders need a driver in seconds, not hours — any fix has to preserve near-instant matching, not trade correctness for an unusable delay.

  2. Randomizing doesn't improve on greedy's core flaw — it just replaces "locally good but globally suboptimal" with "not even locally good," which is strictly worse.

  3. A short batching window (seconds, not minutes) barely delays any individual rider, but gives the matcher visibility into MULTIPLE riders and drivers simultaneously, so it can solve a proper assignment problem (conceptually similar to the Hungarian algorithm for bipartite matching) that minimizes total cost across the group — routinely better than whatever a greedy, one-at-a-time pass would have produced, for a negligible latency cost.

Introduce a Batch Matcher: collect pending requests and available drivers over a short window, then solve matching as an assignment-optimization problem across the whole batch — minimizing total cost (aggregate ETA) rather than optimizing each pair in isolation. A few seconds of batching buys a meaningfully better global outcome for a cost riders barely notice.

Why this piece earns its place

An optimizer serves the objective you wrote, and the objective as stated says nothing about any individual rider. Minimizing total ETA across a window can be achieved by serving most of the batch quickly and deferring one — the rider on a cul-de-sac, or across the bridge — because moving any driver to them costs more than it buys. That is the correct answer to the problem we posed. The trouble is that the next window poses the same problem over the same geography, so the same rider loses again, and nothing notices: every individual solve looks optimal, the aggregate wait-time chart looks healthy, and one person sits watching the app. Deferral therefore has to cost something. Carry the time a rider has already waited into their cost in the next solve, and let it grow, so the optimizer stops finding them cheap to skip and eventually eats the long pickup rather than the accumulated penalty. Set that growth deliberately: flat and it never bites; steep and one awkwardly-placed rider drags an entire window of matches toward them.

  • secondstypical batch window
  • assignment problemsolved per window
  • lower total ETAthan greedy, on average

What the new pieces do

Batch Matcherservice
Solves matching for a whole window of pending riders and drivers together as an optimization problem, not one greedy pair at a time.

Step 5 · Ingesting driver locations at scale

A write-heavy problem, not a read-heavy one

Every online driver pings their location every few seconds. At real scale, that's an enormous, continuous write volume hitting the geo index — a very different load pattern than the mostly-read search traffic other systems deal with. What does that demand from the index?

RiderDriverDispatch APIDriver Geo IndexETA / RoutingBatch MatcherSurge Pricing
The system as it stands at this step. · swipe to pan the diagram

The Driver Geo Index has to be optimized for high write throughput — frequent location updates from potentially millions of concurrently-online drivers — while still serving fast "who's nearby" spatial queries. In practice this means partitioning the index geographically (so writes and reads for one city's drivers don't contend with another's), keeping each update lightweight (overwrite last-known-location, not append an unbounded history), and treating slightly-stale location data as acceptable — a driver's last known ping from 3 seconds ago is a fine approximation of where they are now.

Why this piece earns its place

Overwriting last-known-location is the right call, and it quietly costs two things. The first is history. The index becomes a present-tense view, so nothing in it can later answer why a particular match was made, or replay a bad window to see what the matcher actually saw. Append the raw ping stream somewhere else, asynchronously and off the hot path, or that question is unanswerable forever afterward — and it is also the history Step 8 needs to smooth a noisy signal, which is why smoothing has to happen at ingest, before the overwrite. The second cost is the ping interval, a genuine knob with a price on each side. Ping faster and you buy freshness with write volume, driver battery and more raw noise to filter. Ping slower and you rank on where the driver was — harmless when they are a long way out, decisive when two candidates are a block apart, and far worse for a car at highway speed than a parked one. Make the rate adaptive rather than uniform, and carry heading and speed with the ping so the matcher can reason forward from the last one.

Step 6 · Cancellations and fast re-matching

A driver cancels — now what?

A driver accepts a match, then cancels (traffic, changed their mind, a better offer). The rider is now waiting with no driver at all.

RiderDriverDispatch APIDriver Geo IndexETA / RoutingBatch MatcherSurge Pricing
The system as it stands at this step. · swipe to pan the diagram

Detect the cancellation immediately and re-queue the rider into the next matching window with priority (they've already waited once) rather than making them submit a fresh request. The freed driver simultaneously becomes available again for the same or the next window — cancellations are just another input to the ongoing batch-matching loop, not a special, separately-handled case.

Why this piece earns its place

Re-queueing is the right mechanism and it needs one guard, because as described it quietly pays for cancelling. If a cancelled driver drops straight back into the pool and the next solve ranks on cost alone, the driver who just walked away from a long pickup is immediately eligible for the short one that tempted them — and may even be re-offered the very rider they abandoned, who now gets the same driver back with a longer wait attached. So exclude a cancelling driver from that rider’s candidate set for the re-match, and record the cancellation against the driver rather than treating it as a free event; a marketplace works out which behaviours are profitable much faster than you would like. The word priority on the rider side wants pinning down too, because it should not arrive here as a second mechanism. Express it as time already spent: fold the wait that preceded the cancellation into the accumulated-wait term the objective is already carrying, rather than raising a separate re-queue flag the matcher consults before it solves. Two independent things that both decide who stops being skipped will eventually disagree about the same rider, and whichever one shouts louder wins for reasons nobody wrote down — and neither can be tuned without moving the other. One term, one weight, and a cancellation simply makes a rider expensive to defer for the same reason a long wait does.

Step 7 · Fairness for drivers

Don't always pick the single best driver

Pure cost-minimization always routes requests toward whichever driver has the best ETA. Over time, that concentrates rides (and earnings) on drivers who happen to sit in high-density pockets, while others in lower-demand areas stay idle far longer. Is "always pick the mathematically best match" actually the right objective?

Batch Matchercost + fairnessIdle-Time Ledgerdriver fairness
New in this step: Idle-Time Ledger.

Pure ETA-minimization concentrates rides on a few well-positioned drivers. Is that the right objective for the marketplace?

  1. Optimizing ONLY for rider wait time, with zero regard for driver earnings distribution, degrades marketplace health over time: drivers who consistently get few rides leave the platform, which eventually hurts riders too by shrinking overall supply.

  2. Adding an idle-time factor to the cost function is a deliberate, small trade of pure efficiency for marketplace health: riders experience a negligibly different wait on average, while driver earnings and idle time distribute more evenly, keeping more drivers engaged and online long-term — which ultimately benefits riders too through better overall supply.

  3. Ignoring ETA entirely in favor of pure randomness would make wait times unpredictably bad for riders — fairness should be a MODERATING factor on top of ETA-based cost, not a replacement for it.

Blend the matching cost function with an idle-time fairness factor, tracked in an Idle-Time Ledger per driver: a driver who's been waiting longer gets a small cost-reduction bonus in the matcher's objective, so they occasionally win a match over a marginally-closer driver who just dropped off a rider. This is a deliberate, bounded trade of pure per-ride efficiency for long-run marketplace health.

Why this piece earns its place

A ledger is only as good as what it counts, and idle time can be earned two very different ways. One driver is idle because the matcher kept choosing someone marginally closer — exactly the person this term exists to protect. Another is idle because they parked in a quiet residential pocket where no requests originate, and a bonus that grows with raw clock time rewards them for staying there. Pay that long enough and you have taught part of the fleet to wait where demand is not, which is the opposite of what a dispatch system wants its supply doing. So accrue idle time where being idle was the system’s decision rather than the driver’s: count it while a driver sits inside an area that actually produced requests they could plausibly have been offered, and slow or stop the clock where there was nothing to lose. Cap the bonus as well, so a long idle streak cannot outweigh the ETA term outright — this factor is meant to break ties between comparable drivers, not to send a rider the car on the far side of town.

What the new pieces do

Idle-Time Ledgerdata
Tracks how long each driver has been idle, feeding a fairness factor into matching so earnings and offers don't concentrate on the same few drivers.

Step 8 · The sharp edges

Surge floods, GPS drift, and no-shows

Real-world stress tests: a sudden massive demand spike (handled by batching, from Step 4), noisy or drifting GPS signal producing bad ETA estimates, and a matched driver who never actually shows up.

RiderDriverDispatch APIDriver Geo IndexETA / RoutingBatch MatcherSurge PricingIdle-Time Ledger
The system as it stands at this step. · swipe to pan the diagram

GPS drift is dampened by smoothing location updates (discard implausible jumps, weight recent pings) before they feed the geo index, so a single noisy ping doesn't send a driver on a phantom route. A driver no-show is detected by a timeout on pickup confirmation, which immediately triggers the same re-matching path as an explicit cancellation from Step 6 — the rider is re-queued with priority rather than left stranded indefinitely.

Why this piece earns its place

The no-show timeout looks like a constant and should not be one. A wait that clearly means abandonment at a downtown curb is unremarkable in an airport queue or a stadium lot, where crawling to the pickup point legitimately takes a while, so one global number either strands curbside riders or fires constantly wherever pickups are slow by design. We already computed an honest, local number in Step 2: the predicted pickup ETA. Judge lateness against that estimate plus a margin and the timeout adapts to the geography for free. The two failure directions are not symmetric either. Fire late and a rider waits on someone who was never coming. Fire early and you burn two drivers on one ride and hand the rider a fresh, longer wait while their original driver was pulling around the corner — a worse outcome than the one being fixed. So before declaring a no-show, ask the thing the geo index already knows: if the matched driver’s position is still closing on the pickup point, they are late, not gone, and the right response is to say so, not to re-match.

You did it

You just designed a ride-matching engine.

RiderDriverDispatch APIDriver Geo IndexETA / RoutingBatch MatcherSurge PricingIdle-Time Ledger
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Straight-line distance is a bad proxy for real travel time — a proper routing/ETA service is the true cost function for matching.
  • Matching greedily, one rider at a time, is provably suboptimal — batching a short window and solving it as an assignment-optimization problem produces a better overall outcome for a negligible latency cost.
  • Surge pricing is a genuine market-clearing mechanism: it rations demand AND pulls additional supply toward a shortage, which pure matching algorithms can't do on their own.
  • Driver location ingestion is a write-heavy workload, unlike most search/matching read paths, and needs to be designed and partitioned accordingly.
  • Cancellations and no-shows are handled by re-queueing into the same ongoing batch-matching loop, not a bespoke exception path.
  • A pure cost-minimizing objective concentrates rides on well-positioned drivers — blending in an idle-time fairness factor trades a little efficiency for a healthier, more evenly-utilized driver base.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why is this different from just building "Design Uber"?

    A full ride-hailing platform design (Design Uber) has to cover the ENTIRE product surface — trip lifecycle state machines, payments, driver onboarding, maps rendering, the rider and driver apps — with matching as just one piece among many, usually covered at a summary level. This design goes deep on ONLY the matching and dispatch engine: the actual cost function, the batching-vs-greedy trade-off, the write-heavy location-ingestion problem, and the fairness objective — the specific algorithmic and systems challenges that a one-paragraph "and then we match nearby drivers" glosses over in a full-platform design.

  2. How large can the batching window realistically get before it hurts riders more than it helps?

    In practice, batch windows for ride matching are typically a few seconds — long enough to gather a meaningful pool of pending riders and drivers in a dense area, short enough that no individual rider perceives an unusual delay before being matched. Push the window much longer (tens of seconds or more) and the marginal improvement in assignment quality from a larger batch starts being outweighed by the real cost of every rider waiting that much longer before even being told who's picking them up — the window size itself is a tunable trade-off, not a fixed constant, and can even be shortened dynamically in low-density areas where there's little to batch.

  3. Could the assignment-optimization step become a bottleneck under very high request volume?

    The optimization's cost typically grows with the SIZE of each batch (number of pending riders × drivers being matched together), so the real mitigation is geographic partitioning: solve independent, smaller assignment problems per city or per dense sub-region rather than one enormous global optimization across an entire country's pending requests at once. Riders and drivers in Seattle have no business being jointly optimized against a batch in Miami — partitioning by geography keeps each individual optimization small and fast, which is also consistent with how the geo index itself is partitioned in Step 5.

  4. Why blend fairness into the SAME cost function instead of, say, alternating between a pure-ETA matcher and a pure-fairness matcher?

    Alternating between two separate objectives would produce two different kinds of matches at different times, which is harder to reason about, harder to tune, and doesn't actually let the system make GRADED trade-offs (say, "I'll accept a driver 90 seconds farther away if they've been idle 3x longer, but not if they've only been idle slightly longer"). A single blended cost function lets every match be a continuous trade-off between the two goals, tuned by a weighting parameter, and solved by the exact same batch-optimization machinery already built in Step 4 — one mechanism, one tunable dial, rather than two competing subsystems.

  5. What happens to matching quality in a very low-density area with only one or two available drivers?

    The batch-optimization advantage from Step 4 mostly disappears when there's effectively nothing to batch — with one rider and one driver, ANY correct matcher (greedy or batched) produces the same obvious assignment. What still matters in a sparse area is the ETA-based cost function from Step 2 (still picking the genuinely closest-by-real-travel-time option among a small pool) and surge pricing from Step 3 (which, in a sparse area, is often what's actively working to pull a driver into the area in the first place). The design degrades gracefully to "just match the one reasonable option" rather than needing a separate code path for low-density regions.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Straight-line distance is a poor matching metric because…

    Real ETA via a routing service is the true cost function; map distance is only a rough, sometimes badly wrong, proxy.

  2. Batch matching over a short window beats greedy one-at-a-time matching because…

    Greedy matching can lock in a locally-fine pair that's globally worse than an assignment considering more of the pool at once.

  3. Surge pricing exists mainly to…

    It's a two-sided market-clearing signal — matching alone can't create drivers that don't exist.

  4. The driver geo index is primarily a…

    Frequent location updates from many concurrent drivers make ingestion, not lookup, the dominant load pattern.

  5. Blending an idle-time factor into the matching cost function is meant to…

    Pure cost-minimization concentrates rides on well-positioned drivers; fairness keeps more drivers engaged long-term.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Assign a driver: given a rider request and available drivers, decide which driver gets the ride.
  • Rank by real ETA: use road-network travel time, not straight-line distance, as the matching cost.
  • Batch-match a window: solve pending riders and drivers together as one assignment-optimization problem.
  • Balance the market: surge pricing rations demand and pulls supply when demand outstrips nearby drivers.
  • Ingest at scale, match fairly: absorb write-heavy location pings, and blend an idle-time fairness factor into the objective.

The qualities that shape everything

Each one names the mechanism that buys it.

Optimize the metric riders actually feel
An ETA/Routing service computes real road-network travel time (streets, traffic, turn restrictions) as the matching cost, not straight-line map distance.
Globally better matches than greedy
Collect a short window of pending riders and drivers, then solve one assignment-optimization problem minimizing total ETA across the whole batch.
Handle a shortage matching can’t fix
Surge pricing as a market-clearing signal: raise price in the affected area to ration demand and pull additional driver supply toward the imbalance.
Absorb continuous location pings
A write-optimized, geo-partitioned Driver Geo Index that overwrites last-known-location per ping, treating slightly-stale positions as acceptable.
Cancellations don’t strand a rider
Re-queue the rider into the next matching window with priority and free the driver back into the pool — cancellations are just another input to the batch loop.
Keep the driver base healthy
Blend an idle-time fairness factor into the same cost function the batch matcher already solves, so a long-idle driver occasionally wins over a marginally-closer one.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Real-ETA road-network cost over straight-line map distance

A driver close on a map can be 20 minutes away across a river or highway; the rider experiences wait time, not distance, so map distance is a badly-wrong proxy.

Batch assignment over a short window over greedy one-at-a-time matching

Matching the first rider to their nearest driver can burn a driver who was the best match for a rider arriving a second later; a few seconds of batching buys a globally better assignment for a negligible delay.

Surge pricing over just matching whoever is available

No assignment algorithm can conjure drivers into existence during a shortage — a price signal both rations demand and pulls additional supply toward the imbalance.

A write-optimized geo index over a read-optimized search design by default

Driver location ingestion is genuinely write-heavy — pings every few seconds from many drivers — so overwriting last-known-location and partitioning by region fits the actual load.

An idle-time fairness factor over pure ETA-minimization

Always picking the mathematically closest driver concentrates rides and earnings on a few well-positioned drivers, so idle ones leave — a small efficiency trade for long-run marketplace health.

The answer, out loud

What a strong answer to “Design a Ride-Matching Engine” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Scope the problem

    Let me scope this first, because ride-hailing is a big surface and I don’t want to spend the hour in the wrong part of it. I’m designing the matching and dispatch engine — given a rider who just requested a ride and the drivers online around them, which driver gets it. I’m not designing the trip lifecycle, payments, or the apps. Everything interesting sits inside that one decision: what does close actually mean, do we decide one rider at a time or several together, and what do we do when there simply aren’t enough drivers to go around.

  2. 3–8 min

    The naive version, and its two flaws

    The naive version is one line: find the nearest available driver on the map and send them. I’d write it on the board anyway, because it’s wrong in two independent ways and pulling them apart is most of the design. The metric is wrong — straight-line distance ignores the road network, so a driver who looks close on a map can be twenty minutes away across a river. And the algorithm is short-sighted — matching each rider the instant they arrive can spend a driver who was the better answer for a request landing a second later. Fixing either one leaves the other standing.

    Built in step 1: Nearest driver, by straight-line distance
  3. 8–14 min

    Rank on travel time, not distance

    So the first change is the cost function. I’d put a routing service in the path that computes real road-network travel time — actual streets, current traffic, turn restrictions — and rank candidate drivers on predicted pickup ETA rather than distance. The rider experiences waiting, not distance, so I want the number we optimize to be the number they feel. It’s also the number we show them, which means an ETA that runs systematically optimistic doesn’t only misrank drivers, it makes every match land as a broken promise.

    Built in step 2: Route, don't just measure
  4. 14–20 min

    Where driver positions come from

    That needs somewhere to ask where drivers actually are. Every online driver pings a location every few seconds, so this is a write-heavy workload, which is a little unusual — most search-shaped systems are the opposite, and I’d say so explicitly because it changes the design. I’d partition the index geographically so one city’s writes don’t contend with another’s, keep each update to an overwrite of last-known location rather than an append, and accept slightly stale positions. A ping from a few seconds ago is a fine estimate of where a car is.

    Built in step 5: A write-heavy problem, not a read-heavy one
  5. 20–27 min

    Match the window, not the request

    Now the algorithm. Instead of matching each request the moment it arrives, I’d hold pending requests and available drivers for a short window — seconds — and solve the whole window at once as an assignment problem, minimizing total ETA across all the pairs rather than optimizing each pair alone. That’s the classic greedy-versus-optimal trade, and a few seconds is invisible to a rider next to how much better the overall assignment gets. I’d keep each solve geographically scoped, because riders in one city have no business being optimized against another city’s, and smaller solves stay fast.

    Built in step 4: Solve the whole board, not one move at a time
  6. 27–33 min

    When there aren’t enough drivers

    Matching can only allocate the drivers that exist, though. If a concert lets out and five hundred requests appear in six blocks with forty drivers nearby, no assignment algorithm fixes that — there’s no arrangement of forty cars that is five hundred cars. That’s where price comes in. Raising it in the affected area does two things at once: some riders wait or walk, which lowers demand, and drivers elsewhere head toward it, which raises supply. I’d be clear that this is market-clearing rather than a revenue trick, and that it’s addressing something matching structurally cannot.

    Built in step 3: When demand outstrips nearby supply
  7. 33–38 min

    Cancellations and no-shows

    Then the unhappy paths, which I’d deliberately keep boring. A driver cancels: the rider goes back into the next window with a priority bump, the driver goes back into the pool, and the ordinary batch solve handles both. A driver never turns up: a pickup timeout fires and takes exactly the same path. What I like about that is there’s no bespoke cancellation subsystem — if the pool and the loop already exist, failure is just another input to them, and there’s one code path to get right instead of three.

    Built in step 6: A driver cancels — now what?
  8. 38–43 min

    Who the objective is for

    The last piece is the objective itself. Pure ETA minimization is quietly hostile to the supply side: it concentrates rides on whoever happens to sit in dense pockets, and drivers who rarely get offered anything stop showing up, which eventually costs riders too through thinner supply. So I’d blend an idle-time term into the same cost function the batch matcher is already solving — a driver who’s been waiting a long time gets a small discount and occasionally beats a marginally closer one. One cost function, one weight to tune, and no separate fairness service arguing with the matcher.

    Built in step 7: Don't always pick the single best driver
  9. 43–45 min

    Close on the trade-off

    To close: route for real ETA, batch a short window and solve it properly, price the market when supply runs out, ingest locations write-first, and blend fairness into that one objective. Every one of those is a dial between rider wait and something else — window length against latency, fairness weight against efficiency, price against demand. With more time I’d go after predicting where demand is about to appear, so drivers are repositioned before a shortage rather than priced after it, and I’d want a way to measure the matcher against what a better assignment would have achieved, because otherwise there’s no way to tell whether any of this tuning is improving it.

What this teaches

Learn system design by building the matching and dispatch engine at the heart of a ride-hailing platform — the specific subsystem that decides which driver gets which rider, not the trip lifecycle or payments (see Design Uber for the full product). An interactive guide covering why straight-line distance is a bad proxy for real ETA, why greedy one-at-a-time matching is globally suboptimal, batch matching as an assignment-optimization problem, surge pricing as supply/demand balancing, ingesting driver locations at write-heavy scale, and fairness in who gets matched next.

Key takeaways

  • Straight-line distance is a bad proxy for real travel time — a proper routing/ETA service is the true cost function for matching.
  • Matching greedily, one rider at a time, is provably suboptimal — batching a short window and solving it as an assignment-optimization problem produces a better overall outcome for a negligible latency cost.
  • Surge pricing is a genuine market-clearing mechanism: it rations demand AND pulls additional supply toward a shortage, which pure matching algorithms can't do on their own.
  • Driver location ingestion is a write-heavy workload, unlike most search/matching read paths, and needs to be designed and partitioned accordingly.
  • Cancellations and no-shows are handled by re-queueing into the same ongoing batch-matching loop, not a bespoke exception path.
  • A pure cost-minimizing objective concentrates rides on well-positioned drivers — blending in an idle-time fairness factor trades a little efficiency for a healthier, more evenly-utilized driver base.

Concepts covered

  • What is a matching engine, specifically?
  • Nearest driver, by straight-line distance
  • Route, don't just measure
  • When demand outstrips nearby supply
  • Solve the whole board, not one move at a time
  • A write-heavy problem, not a read-heavy one
  • A driver cancels — now what?
  • Don't always pick the single best driver
  • Surge floods, GPS drift, and no-shows
built to match the whole board well, not just the nearest pair greedily — make the calls, flood a surge event, run the gauntlet.
Finished this one? 0 / 65 System Designs done
NextDesign a Notification System Closest system design to this one

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs