Company questionsGroundworkOla

System design · Chat and real time

Show nearby cabs for riders inside a city

  • Reported once
  • SDE-2
  • 3 diagrams
  • 1 source

One candidate write-up. Treat it as a single data point.

The prompt
Design the feature that shows nearby available cabs on a rider's map. Assume 100,000 drivers online in the busiest city, each reporting a location every 4 seconds, and 150,000 riders with the map open. Also decide whether a pickup point lies inside a defined zone, such as an airport area.

A representative prompt in the style reported, not a quote from Ola. The answer below is written for this site.

Try it first.The answer is covered until you pick. Talk it through against a 45 minute clock, or just read it.

Ask first

Clarify the problem before you draw anything.

Questions I would ask: is the map a browsing view only, or does it also feed booking and driver matching? How fresh must a cab marker be, and is smooth movement on the map required? How many cabs should be shown, and does the rider choose a vehicle type? Which zones matter: airports, no-pickup areas, city service limits, surge areas? How often do zones change?

Assumed scope: drivers report location and status; riders see up to 10 available cabs nearby; a zone check answers "is this point inside zone X". In scope: location ingest, the proximity query, caching, the polygon check. Out of scope: matching and dispatch, routing, pricing, and the trip itself.

Numbers

A back-of-envelope that decides the shape.

Planning figures for the exercise, not facts about any company.

  • Writes: 100,000 drivers ÷ 4 s = 25,000 location updates a second. That is five times the 5,000 committed writes a second a relational primary can be planned for.
  • State size: 100,000 drivers × 100 B = 10 MB. The whole city's live positions fit in memory on one box. The problem is the update rate and availability, not size.
  • Reads: 150,000 riders ÷ 5 s refresh = 30,000 reads a second, past the 20,000 simple reads a second one box does.
  • Grid: a city of about 1,000 km² cut into cells of about 1 km² is 1,000 cells, so about 100 drivers per cell on average; a dense downtown cell might hold 1,000. A query reads a cell plus its 8 neighbours: about 900 candidates on average, a few thousand in the worst case. Distance maths on that is cheap.
  • Cache effect: if each cell's driver list is rebuilt every 2 s, that is 1,000 ÷ 2 = 500 rebuilds a second however many riders ask.
  • Trail logging: storing every ping is 25,000 × 100 B = 2.5 MB a second, about 216 GB a day. Sampling one in five gives about 43 GB.

The numbers force an in-memory, partitioned index, a cell-level cache for reads, and a separate, sampled path for history.

The design

Diagrams first, then components, data and the hard parts.

Nearby cabs at a glancePings go to an in-memory index split by region. Riders never query it directly: they read cell snapshots rebuilt every 2 s.
Driver appsping every 4 sPing gatewayroute by cellRegion shardin memory, hot replicaHistory logevery 5th pingCell snapshotsrebuilt every 2 sRider API9 cells, nearest 10Rider apppolls every 5 sZone servicepolygons in memoryping + seqowner of the cellsampledrebuildmy position9 cell lookups10 cabsis point in zone?12345678
  1. A driver app pings every 4 s with its id, position and a per-driver sequence number.
  2. The ping gateway sends it to the shard that owns the driver's cell. The shard ignores old sequence numbers, moves the driver between cell sets, and expires entries after 12 s without a ping.
  3. Every fifth ping is also appended to the history log, which is off the serving path.
  4. Each cell's driver snapshot is rebuilt every 2 s: 500 rebuilds a second however many riders ask.
  5. A rider app sends its position every 5 s.
  6. The API computes the rider's cell and reads that cell and its 8 neighbours from the snapshots.
  7. It keeps available drivers of the requested type, sorts by straight-line distance and returns 10.
  8. For a pickup point, the API asks the zone service whether it lies inside a zone.

Open on the whiteboardAll 3 diagrams on one tidy board, with a step-by-step walkthrough.

Components. A gateway that accepts driver pings; a location service holding the live index in memory; a read API for riders; a cell-snapshot cache; a log for history; and a zone service.

Data model (in memory).

  • drivers: driver_id -> {lat, lng, cell, status, vehicle_type, seq, updated_at}.
  • cells: cell_id -> set of driver_id, where cell_id is a geohash or similar grid code at roughly 1 km precision. Keep one set per vehicle type if filters are common.

The index is partitioned by region: the city is divided into a few coarse regions, each owned by one shard with a hot replica. A shard owns every cell inside its region, so a rider's 9-cell query normally hits one shard (two near a region edge).

Write path.

  1. A ping arrives with driver id, position, timestamp and a per-driver sequence number.
  2. The gateway routes it to the owning shard by its cell. The shard ignores the ping if the sequence is older than what it holds, which handles out-of-order delivery.
  3. It updates the record. If the cell changed, it removes the driver from the old cell set and adds it to the new one. If the new cell belongs to another shard, send the update there and let the old entry expire; a driver on a boundary can appear in both shards for up to 12 s, so the API de-duplicates by driver id.
  4. Entries expire after 12 s without a ping (three missed pings), so crashed phones disappear without anyone telling us.
  5. Every fifth ping is also appended to a log for trip history and analytics. That log is not on the serving path.

The index is rebuilt from incoming pings within one interval, so it needs no durable database. A replica takes over if a shard dies, and the first pings after a restart refill the rest.

Read path. The rider app sends its position. The API computes the rider's cell and its 8 neighbours, takes each cell's cached snapshot (a list of driver ids and positions, rebuilt every 2 s), filters to available drivers of the requested type, sorts by straight-line distance, and returns 10. The app interpolates markers between refreshes, so movement looks smooth without a faster poll. If fewer than 10 are found, widen to the next ring of cells up to a maximum radius.

Consistency. The map is a hint. A marker can be several seconds old (up to about 6 s when the API reads it: 4 s between pings plus up to 2 s of snapshot age, then the rider's 5 s poll on top), and a driver shown as available may have just taken a trip. The authoritative state lives in the trip service, and dispatch makes a conditional update from available to offered, so two riders cannot get the same driver. This is the line between eventual and strong consistency: browsing tolerates staleness, assignment does not.

Dense and sparse areas. A fixed grid wastes work in a crowded cell and finds nothing in a thin one. Cap candidates per cell by taking the freshest N, and use ring expansion for sparse areas.

Zone check. Polygons (airport, service area, restricted pickup) are loaded into memory on every instance of the zone service, versioned, with a bounding box for each. At load time, record for each grid cell which polygons touch it and whether the cell is fully inside one. A lookup finds the point's cell: if no polygon touches it, answer outside; if the cell is fully inside one, answer inside; otherwise run a ray-casting point-in-polygon test on the few candidate polygons. With 10 candidates of 500 vertices that is 5,000 edge tests, microseconds of work. Define boundary points consistently and support polygons with holes.

Trade-offs

What you chose, and when you would flip.

  • In-memory sharded index vs Redis geo sets. A geo set is quick to adopt and good enough for the first version, but members cannot expire one by one, so you sweep stale drivers yourself and the cell filter options are limited. A custom in-memory index gives per-driver expiry and vehicle-type sets. Start with the managed structure; flip to custom when expiry and filtering get awkward.
  • Fixed grid vs adaptive tree. A fixed grid is trivial and cache-friendly. A quadtree adapts to density but is harder to cache and move between shards. Pick the grid with a cap per cell; flip to a tree if downtown cells routinely exceed a few thousand drivers.
  • Polling vs push for the map. Polling every 5 s is simple and caches by cell. Push gives smoother markers but needs one subscription per rider. Poll for browsing; switch to a push stream only after booking, for one tracked driver.
  • Straight-line vs road ETA. Straight-line distance is fast and good for ranking a list; road time is what the rider cares about but costs a routing call. Use straight-line for the 10 on screen and road time only for the top 3. Flip to road-first where rivers and one-way grids make the two differ widely.

Follow-ups to expect

Where the interviewer pushes next.

  1. A big concert ends and 20,000 riders open the app inside two cells. What breaks first, and how do you protect it?
  2. How would you handle a driver driving along a shard boundary, flipping between two regions?
  3. How would you scale the same design from one city to a whole country?
  4. How would you detect and ignore drivers who send fake locations?
  5. How would the zone check change if zones were updated every few minutes, for surge?

Where candidates slip

The mistakes this question punishes.

  • Writing every ping to a relational table and indexing latitude and longitude. At 25,000 writes a second, the primary cannot keep up.
  • Searching a single cell and missing a cab 50 m away across the cell edge. Always query the neighbours.
  • Treating the map as the booking truth, so two riders are given the same driver.
  • No expiry, so drivers who went offline stay visible as ghost cars.
  • Running the point-in-polygon test against every polygon in the country on each request instead of narrowing by bounding box or cell first.
  • Using straight-line distance as the promised pickup time.

Have a go before you read.

Interviews reward the sketch you make cold. Clarify out loud, draw it, then compare with the answer.

Now make it stick

Where this was reported

Public pages only. Companies do not confirm these, and they rotate their questions, so treat each link as one person's account or one list's tag.

Read these chapters first