Skip to content

03. Ride-Hailing Service — Interview Q&A

~15 min read · Part 4 of 4 (Overview → HLD → LLD → Q&A)

These are the questions an interviewer actually asks once the diagram is on the board. Each answer is the strong version, followed by the wrong answer that quietly sinks candidates.

Q1. How do you index driver locations so "who is near this pin" is fast? Turn each (lat, lng) into a geographic cell id and keep an in-memory map from cell to the drivers in it, so a nearby-search is a handful of bucket reads rather than a scan over millions of drivers. The candidate gather is "read this cell and its k-ring of neighbors." For the cell scheme, H3 (hexagons) is preferable to a geohash grid because all six hexagon neighbors are equidistant, so a k-ring is a uniform disk of coverage with no directional bias, whereas rectangular geohash cells over-cover in one direction and have the prefix-boundary problem where adjacent points share no prefix. Common wrong answer to avoid: "Query the database for all drivers within a lat/lng bounding box." A WHERE lat BETWEEN … AND lng BETWEEN … over a durable table can't keep up with 1.25M writes/second and a full-table geo scan per request; the whole point is an in-memory cell index, not a SQL range query.

Q2. Why not persist driver locations durably? Because the data has a ~4-second half-life — every entry is overwritten by the next GPS update — so durability would mean writing 1.25M rows/second to disk to store values that are worthless before they flush. Location is a fast-decaying signal to index, not a record to persist. Losing a Geo Index shard is self-healing: the next round of updates 4 seconds later repopulates it. Durable state is reserved for trips, where losing data is unacceptable. Common wrong answer to avoid: "Store every location update in the database for accuracy and history." That's ~10 TB/day of raw firehose for data nobody queries; history, if needed, is downsampled aggregates, not the raw stream.

Q3. A rider requests downtown at 6pm with ~500 drivers nearby — walk me through the match. Compute the pickup's H3 cell and gather its k-ring (≈19 hexes, a ~2 km disk) from the Geo Index — about 500 driver states. Filter to available (say ~180 idle), then prune before spending on ETA: sort by cheap haversine distance and keep the closest ~20, which cuts routing cost 9×. Compute road-network ETA for just those 20 and rank — the nearest-by-line car may be 6 minutes away across a one-way street while the third-nearest is 2 minutes down a clear avenue, so ETA, not distance, picks the car. Read the surge multiplier (~1.8× at 6pm), return 202 to the rider immediately with the 2-minute ETA, then run the dispatch loop: offer to #1, wait ≤15s, fall through on decline. Time-to-match lands around 2–4 seconds. Common wrong answer to avoid: "Compute the ETA for all 500 nearby drivers and pick the minimum." That's 500 routing queries per request — at 1,000 requests/second that's 500k ETA calls/second. Pruning to ~20 first is what makes the budget.

Q4. Match latency vs match optimality — how do you resolve it? Do a bounded local search, not a global optimization. The globally optimal assignment minimizes total wait across every pending request city-wide, which is an assignment problem that blows the seconds-level latency budget. Instead we search a small radius, prune to a couple dozen candidates, and offer the locally-best driver. We knowingly accept a slightly-worse-than-perfect match that arrives in 2 seconds over a perfect one that arrives in 20. The framing: matching is not a global optimization problem, it's a bounded local search. Common wrong answer to avoid: "Solve for the assignment that minimizes total system wait time." Correct in a batch-scheduling textbook, wrong for a rider staring at a spinner — it doesn't fit the latency budget and ignores that drivers accept or decline in real time.

Q5. How often should drivers report location, and why does it matter? Every ~4 seconds as a default, with adaptive frequency layered on. The interval is a direct cost dial: at every-4-seconds we sustain 1.25M writes/second; halving to every-2-seconds doubles the firehose to 2.5M/second and doubles ingest fleet and bandwidth for a car that moved ~25 meters. So report often when moving fast or carrying a watched passenger, and back off to every 8–10 seconds when parked and idle — which cuts the aggregate firehose by a third or more because a large share of online drivers are stationary between trips. Common wrong answer to avoid: "Report as frequently as possible for accuracy, like every second." That's 5M writes/second for marginal positional gain and a crippling ingest and mobile-data bill; freshness past a few seconds buys nothing a rider can perceive.

Q6. How do you guarantee one driver is never assigned to two riders at once? Split location (eventually consistent, sloppy, in-memory) from assignment (strongly consistent). The driver's authoritative assignment lives in a driver_assignment row, and the claim at dispatch is a compare-and-set: UPDATE … SET ride_id=:r WHERE driver_id=:d AND ride_id IS NULL. Exactly one concurrent claim affects a row; the loser sees zero rows changed and falls through to its next candidate. The location index — where a driver might still read available for a second after being claimed — never arbitrates this; only the CAS does, and the matcher re-checks it at claim time. Common wrong answer to avoid: "Check the driver's status in the location index, and if available, assign them." That status is stale by design, and two matchers can both read available and both assign — a check-then-act race that double-books the driver.

Q7. How does the system survive the 6pm downtown hot cell? That one cell absorbs ~125 location writes/second from 500 drivers plus a request storm from every rider in the area, concentrated on one Geo Index shard. Unlike a read cache problem, you can't cache the answer — it changes every 4 seconds — so the defense is finer partitioning and bounded reads, not caching. Size downtown cells at a finer H3 resolution so drivers spread across more shards; cap the candidate scan so a match reads the nearest ~50, not all 500; and give the known hot shard dedicated headroom during peaks. The goal is that the busiest cell degrades to slightly-slower matches, never to no matches. Common wrong answer to avoid: "Put a cache in front of the geo query." Caching a query result that's stale in 4 seconds either serves wrong drivers or invalidates constantly; the hot-cell fix is partitioning and read bounds, the opposite of the URL-shortener's cache answer.

Q8. Why compute ETA over the road network instead of straight-line distance? Because straight-line distance systematically mis-ranks candidates. A driver 400 m away across a river with no nearby bridge is 12 minutes out; one 900 m away down a clear avenue is 3 minutes. We use haversine only as a cheap prune to get the right candidate set (closest ~20), then a road-network shortest-path query with live traffic weights to pick the right car within that set. Production layers a learned correction on top for pickup friction — finding the passenger, parking, one-way streets — which is the gap between a naive ETA and one riders trust. Common wrong answer to avoid: "Use Euclidean/haversine distance for the ETA — it's close enough." It's close enough to prune, not to rank; distance ignores rivers, one-ways, and traffic, and confidently offers the trip to a car that's physically near but 12 minutes away.

Q9. How does surge pricing get computed and why does it exist? Per cell, keep a rolling window (say 2 minutes) of request count as demand and read available-driver count in the cell's k-ring as supply; the multiplier is a bucketed function of the demand:supply ratio (ratio 1–2 → 1.5×, 2–4 → 2.0×, capped), recomputed every few seconds and published for the matcher to read in one lookup. It exists to do two things at once: ration scarce cars toward riders who value the trip most, and pull more drivers into the hot cell by raising their fare. It's intentionally cheap and lossy — a lost computation just recomputes next tick. Common wrong answer to avoid: "Set one global surge multiplier for the whole city." Surge is inherently local — one neighborhood at a concert can be 3× while two miles away it's 1× — so a global multiplier both over-charges calm areas and fails to attract drivers to the specific hot cell.

Q10. What happens when a Geo Index shard fails? The cells it owned go dark: drivers there disappear from candidate sets and riders there can't match for a few seconds. Because the data is ephemeral, recovery is automatic — stand up a replacement and the next round of location updates repopulates it within one update interval (~4 s). To shrink the gap below that, replicate hot shards and fail reads over to a replica so the outage is sub-second. This is fundamentally different from losing durable data, which is why we accept in-memory-only here. Common wrong answer to avoid: "We'd lose driver data and have to rebuild from a backup." There's no backup to restore — the data is regenerated by live updates in seconds; treating an ephemeral index like a durable store leads to the wrong recovery design.

Q11. What's the shard key for the geo index, and why not shard by driver id? Shard by geographic cell, so a region's writes and its match queries co-locate on the same node — a k-ring query then touches one or a few shards instead of fanning out to all of them. Sharding by driver id would scatter the ~500 drivers near a pin across every shard in the fleet, turning one nearby-search into a fan-out to the entire cluster. The trip store, by contrast, shards by region then trip id, because its queries are point lookups and per-user history, not spatial. Common wrong answer to avoid: "Shard by driver id for even distribution." Even distribution of writes at the cost of scattering every spatial read across all shards — it optimizes the cheap operation and wrecks the expensive one.

Q12. A driver taps accept at second 16, after the offer timed out and #2 already took the trip. What happens? The late accept fails the claim CAS — the assignment row is no longer null / the ride is taken — and returns 409, and the driver is told the trip's gone. The offer also carries a client-visible expiry, but the server-side CAS is the real guard; you never trust the client's clock to decide who won. This is the same mechanism that resolves two simultaneous accepts: the store, not the app, arbitrates, and exactly one wins. Common wrong answer to avoid: "Honor the accept since the driver did tap it." That either double-books (two drivers think they have the trip) or requires trusting client timestamps; the atomic claim must be the single source of truth for who got the ride.

Deeper follow-ups

  • How would you extend matching to pooled/shared rides, where a driver's route can absorb a second rider mid-trip without adding more than a few minutes to the first?
  • How do you keep ETAs honest when a driver's GPS drifts or they take a route different from the one the router assumed — do you re-estimate, and how does that change the rider's countdown?
  • How would you batch dispatch during extreme peaks (offer to a small set and optimize assignment across a 1–2 second window) versus greedy first-come dispatch, and what does the rider experience trade?
  • How do you prevent surge from oscillating (a multiplier that jumps 1× ↔ 2.5× every few seconds as supply reacts), and what smoothing or hysteresis would you add?
  • How would you run matching across regions/data centers so a rider near a boundary can be matched to a driver in the neighboring shard without a global lock?
  • How do you detect and handle GPS spoofing or a driver reporting a fake position to farm surge fares?

How this round is scored

Interviewers use ride-hailing to see whether you recognize the two-plane structure — a write-heavy, ephemeral, eventually-consistent location plane versus a strongly-consistent trip/assignment plane — and refuse to collapse them into one durable store. The strong signal early is naming the real asymmetry (1.25M location writes/second against ~1k match requests/second) and letting it drive an in-memory geospatial index rather than a database geo query. Seniority shows in the tradeoff discussions — match latency vs optimality (bounded local search), location frequency vs cost (the firehose dial), consistency of driver state (CAS claim over stale location) — where you name both sides and pick with a number attached. The geospatial-index choice (H3's uniform neighbors vs geohash's boundary problem vs a quadtree's rebalancing) is where depth separates candidates who've built these from those who've only drawn them. And the double-dispatch guard is the correctness question the round hinges on: a candidate who resolves it with an atomic claim rather than a status check has understood that location can be sloppy but assignment cannot.