Design live ride tracking¶
Design the rider and driver apps of a ride-hailing service, focusing on the trip itself: the rider books, a driver accepts, and the rider watches the driver arrive on a map and follows the trip until drop-off.
Difficulty: π΄ Senior Β· Core topics: location updates, real-time streams, trip state machines, battery, map rendering
Requirements¶
Functional
- The rider requests a ride and sees "finding a driver", then the assigned driver.
- The rider sees the driver's car moving on the map, with an ETA, until pickup and during the trip.
- The driver app sends its location while online and handles accept, arrived, start and end trip.
- Both apps survive the app being backgrounded or killed mid-trip.
Out of scope: matching and pricing algorithms, payments (see UPI payments app), ratings.
Non-functional
| Requirement | Target |
|---|---|
| Freshness | The car on the rider's map is at most 5 seconds behind |
| Smoothness | The car glides along the road, never teleports or drives through buildings |
| Battery | A driver can work a 10-hour shift; the rider app costs little while waiting |
| Reliability | Trip state is always correct, even after a crash, on both apps |
| Networks | Works on patchy 3G/4G, including tunnels and dead zones |
Estimates¶
- 200,000 drivers online at peak, each sending a location every 4 seconds: 50,000 updates a second. Each update is about 50 bytes in protobuf, so 2.5 MB/s in total. The volume is in the number of writes, not the bytes.
- A driver's phone sends 9,000 updates over a 10-hour shift: about 450 KB of upload. Trivial for data, but every radio wake-up costs battery, so batch when you can.
- A rider watching a trip receives one update every 2β4 seconds: tiny.
Architecture¶
flowchart TB
subgraph DRIVER[Driver app]
GPS[Fused location provider] --> FILTER[Filter and batch]
FILTER --> DSOCK[Socket / HTTP sender]
DTRIP[Trip state machine] --> DDB[(Local DB)]
FGS[Foreground service<br/>with notification] --> GPS
end
subgraph Backend
LOCIN[Location ingest] --> GEO[(In-memory geo index)]
LOCIN --> STREAM[Pub/sub per trip]
TRIP[Trip service] --> TRIPDB[(Trip DB)]
MATCH[Matching] --> GEO
ETA[ETA service] --> STREAM
end
subgraph RIDER[Rider app]
RSOCK[Socket client] --> RTRIP[Trip state machine]
RSOCK --> INTERP[Interpolate and snap to road]
INTERP --> MAP[Map view]
RTRIP --> RDB[(Local DB)]
end
DSOCK --> LOCIN
DTRIP --> TRIP
STREAM --> RSOCK
TRIP --> STREAM
- Location and trip state are separate streams. Locations are frequent and disposable: losing one doesn't matter. Trip state changes are rare and must never be lost.
- Live locations live in memory (a geo index such as Redis geospatial sets or an H3 grid), not in the main database.
- The rider only subscribes to one driver's stream, for one trip.
Data model¶
Trip (both apps, local DB and server)
id, status, rider_id, driver_id, pickup, dropoff,
status_version increases with every change
updated_at
LocationUpdate (in flight only)
driver_id, trip_id?, lat, lng, accuracy_m, heading, speed_mps,
recorded_at (device time), seq
API¶
Driver β server (WebSocket, protobuf, or HTTP batch when the socket is down)
location { seq, points: [ {lat, lng, accuracy, heading, speed, t} ] }
trip_event { trip_id, event: arrived | started | ended, idempotency_key }
Server β rider (WebSocket, subscribed to trip_id)
driver_location { lat, lng, heading, t, eta_seconds }
trip_status { status, status_version, driver {...} }
REST (source of truth for state)
GET /v1/trips/{id}
POST /v1/trips (request a ride, idempotency key)
POST /v1/trips/{id}/events (driver events, idempotency key)
Deep dives¶
1. The trip state machine¶
stateDiagram-v2
[*] --> Requested
Requested --> DriverAssigned: driver accepts
Requested --> NoDriver: timeout
DriverAssigned --> DriverArrived: driver taps Arrived
DriverAssigned --> Cancelled: rider or driver cancels
DriverArrived --> InProgress: driver starts trip
DriverArrived --> Cancelled: no-show
InProgress --> Completed: driver ends trip
Completed --> [*]
Cancelled --> [*]
NoDriver --> [*]
- The server owns the state. Apps send events (with idempotency keys) and render the state the server returns.
- Every change has a
status_version. An app ignores any update with a lower version than the one it has, so out-of-order messages (socket and push arriving together) can't move the trip backwards. - On launch, both apps fetch the active trip with
GET /trips/{id}. A rider who reopens the app mid-trip lands straight back on the live map. - Driver events that fail to send go into an outbox and are retried. "Trip ended" sent from a tunnel must still arrive.
2. Sending the driver's location efficiently¶
- Use the fused location provider (Android) or
CLLocationManager(iOS) with settings that depend on the driver's state:
| Driver state | Interval | Accuracy |
|---|---|---|
| Offline | Off | β |
| Online, waiting | 10β15 s, or on 50 m moved | Balanced |
| Going to pickup / on trip | 2β4 s | High |
- Filter points with poor accuracy (say, over 50 m) and impossible jumps (over 200 km/h).
- When the socket is down, buffer points and send them as a batch when it reconnects. Old points still help the server compute distance and the fare, even if the rider's map only needs the latest.
- Android needs a foreground service with a visible notification to keep receiving locations in the background; iOS needs the location background mode and "Always" permission. Explain why to drivers before asking.
3. Making the car move smoothly on the rider's map¶
Raw updates every 3 seconds make the car jump. Instead:
flowchart LR
U[Update: position + time] --> SNAP[Snap to road<br/>on the server or with map data]
SNAP --> BUF[Buffer of recent points]
BUF --> ANIM[Animate between points<br/>over the update interval]
ANIM --> ROT[Rotate the car icon<br/>to the heading]
- Render a little in the past (about one update interval). Then you always have a next point to animate towards, rather than guessing.
- If updates stop, keep the car still and show "Driver's location may be delayed" after about 15 seconds. Don't extrapolate for long: guessing wrongly looks worse than pausing.
- Animate with the map SDK's marker animation on the UI thread, and keep the camera stable. Don't re-centre on every update while the user is panning.
4. ETA¶
- ETA comes from the server (it knows traffic), sent with location updates.
- Smooth what the user sees: don't let "4 min" flip to "6 min" and back on every update. Change it only when the difference persists.
5. Battery and data on the rider app¶
- Subscribe to driver updates only while the trip screen is visible. In the background, rely on push notifications for state changes ("Your driver has arrived").
- On Android, show an ongoing notification; on iOS, a Live Activity shows the ETA on the lock screen without the app running.
Failure modes¶
| What happens | Driver app | Rider app |
|---|---|---|
| Driver in a tunnel for 2 minutes | Buffers points; events go to the outbox | Car stops; "location may be delayed" |
| Rider app killed mid-trip | β | Reopen: fetch the active trip, resume the live map |
| Driver app killed by the OS | Foreground service makes this rare; on restart, fetch the active trip and resume tracking | Location stale; trip state unaffected |
| Socket messages arrive out of order | β | Lower status_version updates ignored |
| "End trip" tapped with no network | Outbox retries with the same idempotency key; UI shows "Ending tripβ¦" | Sees "Completed" once the server receives it |
| Phone GPS returns a wild point | Filtered by accuracy and speed limits | Never sees it |
Trade-offs¶
- Update frequency vs battery and server load: every second looks great but multiplies cost. Every 3β4 seconds with interpolation looks almost the same.
- WebSocket vs MQTT vs HTTP polling: WebSocket is simple and fine for riders. MQTT handles flaky mobile links and many small messages well, which suits driver location. HTTP batching is the fallback for both.
- Snapping on the server vs on the device: the server has better map data and the same answer for everyone; the device saves a round trip. Snap on the server, interpolate on the device.
- Separate driver and rider apps vs one app: separate apps keep the driver app's permissions and battery profile away from riders. Almost every ride-hailing company ships two.
Interview follow-ups¶
- How do you show nearby cars before booking? A cheap, cached query of the geo index by map area, refreshed every 10β15 seconds, with fuzzed positions for driver privacy.
- How does the rider share the trip with family? A share link to a web page that subscribes to the same trip stream with a limited, expiring token.
- How would you test this? Replay recorded GPS traces (including tunnels and bad accuracy) through the driver app's location layer, and drive the state machine with a fake server that delays and reorders messages.
Related: Building blocks: real-time updates Β· Building blocks: background execution