Design problems
Maps, mobility & logisticsHardSDE-2 35 min

Design ride hailing (Uber / Ola)

Asked at
OlaRapidoSwiggyUberZomato

The interviewer asks

“Design a ride-hailing service like Uber or Ola. Riders request a ride, see a fare and ETA, get matched to a nearby driver, track the driver live, and pay at the end of the trip.”

What a strong answer looks like

A strong answer is built around two hot paths. The first is location: hundreds of thousands of online drivers send GPS updates every few seconds, which are written to an in-memory geospatial index (H3 cells or geohash) partitioned by city and cell, not to a database on every ping. The second is dispatch: when a rider requests, the dispatch service finds candidate drivers in nearby cells, ranks them by road ETA (not straight-line distance), and offers the trip to one driver at a time with a timeout, using an atomic lock so a driver can never get two trips. Around these sit a trip service with an explicit state machine in a strongly consistent store, a pricing service that issues a signed upfront fare quote, real-time push over WebSockets for tracking, and an async payment and receipt pipeline. The deep dives are the location index, matching, the trip state machine, and handling city-level hot spots and network loss in Tier-2 cities.

Concepts to mention

In-memory geospatial index with H3 or geohash cellsLocation pings not written to a primary databaseRing search around the pickup cellRank by road ETA, not straight-line distanceAtomic driver offer lock with TTL (lease)Trip state machine with conditional updatesWebSocket gateway with connection registrySigned upfront fare quoteKafka for location trails and trip eventsPartitioning by city

1 Requirements

Functional

  • Rider enters pickup and drop, sees vehicle types with upfront fare and pickup ETA
  • Rider requests a ride; the system matches a nearby available driver within seconds
  • Driver receives the offer and accepts or declines within a time limit
  • Both sides see live location: rider sees driver approaching and the trip on a map
  • Trip lifecycle: arrived, OTP-verified start, end; fare computed; payment by cash, UPI, wallet or card
  • Ratings, trip history and receipts
  • Cancellations by rider or driver with cancellation fee rules

Non-functional

  • Match a driver within ~5-10 s for most requests in served areas
  • Driver location visible to the rider with under ~3 s delay
  • Handle ~500k concurrent online drivers sending location every 4 s (~125k writes/s), peak ~200k/s
  • ~10M trips/day, ~1,000 ride requests/s at peak (office hours, rain, events)
  • A driver is never assigned two trips at once; a trip is never lost or charged twice
  • 99.99% availability for request and dispatch; degrade gracefully per city
  • Works on flaky mobile networks: short disconnects must not cancel a trip

Out of scope (say it out loud)

  • Surge pricing algorithm in detail (separate design)
  • Map rendering and turn-by-turn routing engine internals (separate design)
  • Driver onboarding, background checks and payouts
  • Pooling / shared rides (mention as a follow-up)

2 Estimates

Location writes~125k/s, peak ~200k/s500k online drivers ÷ 4 s interval = 125k/s; peak supply ~800k → 200k/s
Ride requests~115/s average, ~1,000/s peak10M trips/day ÷ 86,400 ≈ 115/s; requests > trips because of retries and cancels; rain and evening peaks ~5-8x
Fare quotes~5,000/s peakRiders open the app and check prices ~5x per actual request
Tracking pushes~250k/s~1M active trips + driver-en-route at peak × 1 update / 4 s ≈ 250k pushes/s to riders
Live location memory< 1 GB800k drivers × ~100 bytes (id, lat, lng, heading, status, ts) ≈ 80 MB; plus indexes, still small, so it fits in memory per city shard
Trip storage~20 GB/day trips, ~1 TB/day GPS trails10M trips × 2 KB ≈ 20 GB; route trails: 10M × 25 min × 15 points/min × ~40 bytes ≈ 150 GB; plus all idle pings if kept, ~1 TB/day in S3

3 API

EndpointWhat it does
POST/v1/fare-quotespickup, drop, vehicle types → per type: fare, ETA, quote_id (signed, valid ~5 min)
POST/v1/tripsRequest a ride with quote_id and payment method. Header Idempotency-Key. Returns trip_id in state MATCHING
GET/v1/trips/{trip_id}Trip state, driver details, ETA; used for recovery after reconnect
POST/v1/trips/{trip_id}/cancelCancel by rider or driver with reason; applies fee rules
POST/v1/driver/locationBatch of GPS points (lat, lng, accuracy, speed, heading, ts); usually over the driver's persistent connection
WS/v1/streamPersistent channel: offers to drivers, driver location and trip events to riders
POST/v1/offers/{offer_id}/acceptDriver accepts the offer; succeeds only if the offer is still held for this driver
POST/v1/trips/{trip_id}/startDriver starts the trip after entering the rider's OTP

4 Data model

driver_locationsIn-memory geo index (Redis GEO / custom service), sharded by city + H3 cell
driver_id → latlngheadingstatus (available, offered, on_trip)vehicle_typeupdated_at; cell → set of driver_ids

Overwritten on every ping; entries expire if no ping for ~30 s. Never written to a relational DB on each ping.

driver_stateRedis (with persistence) or the dispatch service's memory
driver_idstatuscurrent_trip_idoffer_lock (trip_id with TTL)last_seen

Atomic SET NX with TTL as the offer lock, so a driver is held for one trip offer at a time.

tripsPostgres / CockroachDB / Spanner-like store, sharded by city or trip_id
trip_id (PK)rider_iddriver_idcity_idstatequote_idfarepickupdroprequested_attimestamps per stateversion

Strong consistency and conditional updates for state transitions. Index on rider_id and driver_id for history.

trip_eventsKafka → S3
trip_ideventatactorpayload

Event stream for billing, analytics, ETA model training and support tools.

location_trailsKafka → S3 / Cassandra
trip_id or driver_idtslatlngspeed

Used for fare (distance), disputes, safety and map-matching. Cassandra partitioned by (trip_id) if support tools need fast reads.

fare_quotesSigned token + Redis
quote_idrider_idroutevehicle_typefaresurge_multiplierexpires_atsignature

Stateless signed quote lets any node validate it; Redis copy for analytics and single-use enforcement.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALGPS every 4 squote, requestupdate celllocation streamfare quoteroute, ETAcreate tripfind drivernearby driversroad ETAsoffer to driverstate machinetrip eventsrider trackingtrip completedDriver appRider appWebSocket gatewayAPI gatewayLocation serviceDispatch / matchingPricing + ETA serviceTrip serviceKafka (locations,trips)Payment workersGeo index (Redis, H3)S3 trails + eventsTrips DB (sharded)Maps / routing enginePayment gateway / UPI
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Request and match a ride

  1. 1Rider asks for a quote; pricing gets the route and ETA from the routing engine, applies base fare, time, distance and the cell's surge multiplier, and returns a signed quote valid for 5 minutes.
  2. 2Rider requests with the quote_id. Trip service validates the signature and expiry, creates the trip in MATCHING (idempotent on the key), and calls dispatch.
  3. 3Dispatch takes the pickup's H3 cell and its rings (k=1, then k=2) and gets available drivers of that vehicle type from the geo index: say 25 candidates.
  4. 4It asks the routing engine for road ETAs from each candidate to the pickup (a one-to-many matrix call) and ranks by ETA, acceptance probability and driver fairness.
  5. 5For the best driver it sets offer_lock = trip_id with SET NX and a 15 s TTL. If the lock fails (driver is held by another trip), it moves to the next candidate.
  6. 6The offer goes to the driver over WebSocket (with a push notification fallback). The driver taps accept within 15 s.
  7. 7Accept goes to the trip service, which runs a conditional update: state MATCHING → DRIVER_ASSIGNED, driver_id = X, only if the trip is still MATCHING. The driver's status becomes on_trip.
  8. 8If the driver declines or times out, the lock expires or is released and dispatch offers to the next candidate; after N tries or 60-90 s, the rider is told no drivers are available.
  9. 9The rider receives the driver's details and live location as trip and location events are pushed through the WebSocket gateway.

During and after the trip

  1. 1Driver arrives (geo-fence check), rider shares a 4-digit OTP, driver enters it; trip moves to IN_PROGRESS.
  2. 2Driver app sends GPS points; the location service tags them with trip_id and streams them to Kafka for the trail and for the rider's live map.
  3. 3At drop, the driver ends the trip. Trip service computes the final fare from the quote (upfront fare) and adjusts only for rules like a changed destination or long waiting.
  4. 4Trip moves to COMPLETED and emits an event. Payment workers charge the UPI/card/wallet with idempotency key = trip_id, or record cash collected against the driver's ledger.
  5. 5Receipts and rating prompts are sent; the trail is archived to S3.

7 Deep dives

Location index for 125k writes per second

The live location of drivers is the most write-heavy data and the least durable: a driver's position 30 seconds ago is worthless. So keep it in memory and never write each ping to a disk-based database. Divide the map into cells. H3 (Uber's hexagonal grid) is a good choice because every hexagon has six equidistant neighbours, which makes 'search the ring around this cell' simple; geohash with neighbour lookups also works. At H3 resolution 8 a cell is about 0.7 km², and resolution 9 about 0.1 km². On each ping, the location service computes the cell; if it changed, it moves the driver from the old cell's set to the new one, and it always overwrites the driver's latest position. Shard by city, then by cell, so Bengaluru's load never affects Pune. A nearby query reads the pickup's cell and rings outward until it has enough candidates, then filters by status and vehicle type. Pings also go to Kafka for trails and analytics, decoupled from the index. Clients send every 4 s while active, less often when idle or stopped, and batch points after a network gap; the server discards out-of-order points older than the latest it has. Stale drivers drop out of the index via TTL.

Matching: road ETA, offers and no double assignment

Nearest by straight line is often wrong: a driver 500 m away across a flyover or a one-way road may be 8 minutes away. Rank candidates by road ETA from the routing engine (one-to-many matrix), then adjust for acceptance likelihood and fairness. Two dispatch styles exist. Greedy: match each request immediately to its best available driver; simple and fast. Batched: collect requests in a city zone for 1-2 seconds and solve an assignment problem across requests and drivers, minimising total pickup time; this gives better global results in busy areas, and Uber has described moving to batched matching. Mention both and choose batched for dense cities. Double assignment is the classic bug: two dispatch workers offering the same driver to two trips. Prevent it with an atomic lock per driver (Redis SET driver:X:offer trip_id NX PX 15000) and by making the accept a conditional update on the trip. The offer is a lease: if the driver does not answer before the TTL, the lock expires and the driver is free again. Keep offers sequential per driver but allow dispatch to prepare the next candidate while waiting, to cut matching time.

Trip state machine and consistency

Trips move through states: MATCHING → DRIVER_ASSIGNED → DRIVER_ARRIVED → IN_PROGRESS → COMPLETED, with CANCELLED_BY_RIDER, CANCELLED_BY_DRIVER and NO_DRIVER_FOUND branches. Store trips in a strongly consistent database and make each transition a conditional write: UPDATE trips SET state = 'IN_PROGRESS', version = version + 1 WHERE trip_id = ? AND state = 'DRIVER_ARRIVED'. This settles races like a rider cancelling at the exact moment the driver accepts: only one update wins, and the loser gets a clear error that the app can show. Every transition emits an event (via an outbox, so the event and the state change commit together) for payments, notifications and analytics. Clients are not the source of truth: after a reconnect, the app calls GET /trips/{id} and redraws from server state. Partition the trips table by city or by trip_id; queries by rider or driver for history use secondary indexes or a separate read model. Cross-region: a trip lives in the region that serves its city, which keeps writes local and fits Indian data localisation preferences.

Real-time tracking over unreliable mobile networks

Drivers in Tier-2 and Tier-3 cities and riders in basements lose connectivity often. Use persistent connections (WebSocket or MQTT) for both apps, with heartbeats every 10-20 s and quick reconnect with jitter. The WebSocket gateway is stateful: it maps user_id to the connection on a specific node, stored in a shared registry (Redis), so the trip service can route a message to the right gateway node. For the rider's live map, subscribe the rider to the assigned driver's location topic; the gateway forwards updates at most every 2-4 s and the app interpolates movement between points so the car moves smoothly. If a driver goes silent for 30-60 s during a trip, do not cancel: mark the location as stale, keep the trip, and let the driver app buffer GPS points and upload them on reconnect, which also keeps the distance and fare correct. Use SMS or push as a fallback for critical events like 'driver arrived' and offers. Keep message payloads small (binary or compact JSON) because data costs and bandwidth matter for drivers.

8 Trade-offs

Greedy vs batched matching

Greedy is simpler and gives the fastest single match but makes poor global choices when demand is high. Batching over a 1-2 s window improves total pickup ETA and driver utilisation in dense areas at the cost of a small delay and more complex code. Use greedy in sparse areas and batched in dense zones.

Redis geo index vs a custom in-memory grid service

Redis GEO (geohash-based sorted sets) is quick to build with and good up to large scale with sharding by city. A custom service holding an H3 grid in memory gives full control over ring searches, filtering and sharding, which large players choose. Start with Redis, partitioned by city, and move when needed.

Upfront fare vs metered fare

Upfront fares give riders certainty and are easier to dispute, but the company carries the risk of traffic and route changes. Metered fares are fair to drivers but uncertain for riders. Most apps now quote upfront and only recompute on clear changes (destination change, long waits).

9 Bottlenecks

Event hot spots

After a cricket match at Chinnaswamy, tens of thousands of requests come from a few cells. One shard holds those cells, and dispatch for that zone becomes hot. Split hot cells to a finer H3 resolution, run batched matching per zone, queue requests with an honest wait estimate, and rely on surge to balance demand.

Routing engine ETA calls

Each dispatch needs a one-to-many ETA matrix; at 10x requests this dominates cost. Pre-filter candidates by straight-line distance, cap the matrix size (say 20), cache ETAs between cells for a minute, and use a fast approximate ETA model for ranking with exact routing only for the final pick.

10 Failure handling

Location service shard down for a city

New matches in that city stop until a replica takes over; drivers reconnect and re-send their location, so the index rebuilds in seconds from fresh pings (no backup needed for such short-lived data). Ongoing trips are not affected because trip state lives in the trips database.

Driver app loses network mid-trip

The trip continues; the app buffers GPS points and uploads them on reconnect. If the driver cannot end the trip, support tools or the app can end it later with the buffered trail. The fare uses the trail, not the live stream.

Payment failure after trip

The trip stays COMPLETED with payment PENDING. Workers retry with the same idempotency key; if it fails, the amount becomes outstanding on the rider's account and is collected before or with the next ride.

11 What impresses

  • Estimating location writes (500k drivers / 4 s) and concluding they belong in memory, not a database
  • Comparing greedy and batched matching and when each fits
  • Preventing double assignment with a lease lock and a conditional accept
  • Handling the rider-cancel vs driver-accept race explicitly
  • Designing for flaky networks: buffered GPS, server-side truth on reconnect, SMS fallback
  • Partitioning everything by city to isolate failures and hot spots

12 Mistakes

  • Storing every GPS ping in Postgres or MySQL and querying with lat/lng ranges
  • Matching by straight-line distance only
  • No mechanism to stop a driver getting two trips
  • Polling the server every second for tracking
  • Cancelling trips when a phone goes offline briefly
  • One global dispatch service with no partitioning

13 Follow-ups

Q1How would you add shared rides (pooling)?
Q2How would you add scheduled rides for the airport tomorrow at 5 AM?
Q3How do you detect GPS spoofing by drivers?
Q4How do you make the driver ETA shown to the rider accurate?
Q5How would you run this across multiple regions?
Q6How would you support bike taxis and autos with different rules?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free