Ride hailing: ask, size and shape it
Part of the free System Design course on LogicWiz, in the chapter: Design ride hailing (Uber). The interactive whiteboard exercises and simulations run in the app.
Week 16: Ten thousand dots on a map
"We saved every GPS ping as a new row. At 9 a.m. the map froze, and riders saw 'no drivers nearby' with a cab outside their gate."
A candidate in a mock interview, after the whiteboard database fell over
Kabir starts the mock interview with one line: "Design a ride-hailing app for Indian cities." Meera reaches for the marker. He raises a hand. "Five minutes of questions first. The answers decide every box you draw."
What the product does, and what to ask
A rider opens the app, sets a pickup point and a drop point, and taps Book. The system finds a free driver nearby and offers the ride. One driver accepts. Then both watch the cab move on a map until the trip ends. That is the whole product: two apps and one trip between them.
Ask four questions first:
- How many drivers are online at peak, and how often does each send a position? Here: 50,000 drivers, one ping (a small location message) every 5 seconds.
- How fast must a match be? Here: a driver found, or "no drivers", within about 30 seconds.
- May the system ever give one driver two trips? Here: never.
- Are fares, routing or payments in scope? Here: no. We call a maps service for road times.
Requirements, with numbers
Functional (what it does):
- Drivers send their location while they are online.
- A rider requests a ride and sees the matched driver and an ETA (estimated time of arrival).
- The system offers the ride to nearby free drivers until one accepts, or says "no drivers".
- Rider and driver both see the trip status and the driver's position.
- Either side can cancel before pickup.
Non-functional (how well):
- About 10,000 location writes per second at peak.
- Nearby-driver lookup with p99 under 50 ms. The p99 is the time that 99 out of 100 lookups beat.
- A match, or "no drivers", within about 30 seconds.
- One driver never gets two trips, and one trip never gets two drivers.
- An accepted trip is never lost. The ride request path is available 99.99% of the time.
Out of scope: surge pricing and fares, the maps engine, payments, ratings and driver onboarding. Say these out loud. The interviewer can pull one back in.
Size it: the arithmetic
- Location writes. 50,000 drivers × 1 ping per 5 seconds = 10,000 writes per second.
- Hot index. Each driver needs about 100 bytes: id, latitude, longitude, status, time. 50,000 × 100 bytes = 5 MB. That fits in the memory of any machine. Size is not the problem. The write rate and freshness are.
- Ping history. 10,000 per second × 100 bytes = 1 MB per second. Times 86,400 seconds in a day = about 86 GB a day. That goes to a stream and cheap storage for disputes and analytics, never into the hot index.
- Ride requests. 2 million rides a day / 86,400 = about 23 per second. Office hours and rain push it to roughly 5 times that: 120 per second. Each request does one nearby lookup and up to 3 offers, one after another.
- Live tracking. At most 50,000 trips can run at once, because each needs a driver. A rider's phone gets a position every 5 seconds. So pushes are at most 50,000 / 5 = 10,000 per second.
Compare 10,000 location writes with 120 ride requests per second. The location path is about 80 times heavier. The ride path is light, but it must be exactly right. Two paths with different needs get two different designs.
Meera's first idea was a drivers table with latitude and longitude columns, updated on every ping. Test it with the numbers. One database primary is comfortable at about 2,000 simple queries per second. Ten thousand writes is 5 times that, before any rider asks for anything. Each write also rewrites index pages, and every replica repeats the work. A latitude and longitude range is a poor query anyway, as the geo lesson showed.
The API
WS /ws/driver driver sends {lat, lng, heading, ts} every 5 s; server sends offers
POST /rides {pickup, drop, idempotencyKey} -> 201 {rideId, state: "MATCHING"}
POST /rides/{id}/accept {offerId} (driver) -> 200 {ride} | 409 offer expired or taken
POST /rides/{id}/events {event: ARRIVED | STARTED | COMPLETED} -> 200 {state}
POST /rides/{id}/cancel {reason} -> 200 {state: "CANCELLED"}
WS /ws/rides/{id} stream of {state, driverLocation, eta}
Three choices to explain out loud:
- Drivers use a WebSocket, a connection that stays open and carries messages both ways. A new HTTP request every 5 seconds from 50,000 phones is wasteful. The server also needs a way to push an offer to a driver.
- POST /rides carries an idempotency key, a unique id the app creates once and reuses on every retry. Mobile networks are weak, so apps retry. Without the key, two retries book two cabs.
- Accept can return 409 Conflict: the offer expired, or another driver got it. That is a normal answer, not a crash.
The data model: split by how bad it is to lose
geo index (memory): cell_id -> set of driver_id
driver_id -> {lat, lng, status, ts}, expires after about 30 s
drivers(driver_id PK, status AVAILABLE | OFFERED | ON_TRIP | OFFLINE, current_ride_id, version)
rides(ride_id PK, rider_id, driver_id NULL, state, pickup, drop, idempotency_key UNIQUE, version)
offers(offer_id PK, ride_id, driver_id, expires_at, outcome)
ride_events(ride_id, seq, from_state, to_state, actor, at) append-only history
ping history: stream -> cold storage
The live position is soft state: data you can lose because it rebuilds itself. If the index vanishes, the next round of pings refills it within 5 seconds. Trips and driver status are durable state and live in SQL. They are small: about 120 new rides per second. Each position carries a TTL (time to live: it deletes itself after about 30 seconds), so a phone that loses signal drops off the map.
Now write the numbers down as you would on the interview board.
📌 Summary: Ask about drivers online, ping rate and the never-twice rule. Pings cost 10,000 writes per second against only 120 rides per second, so keep live positions in memory and trips in SQL.
What Meera learns next
The numbers say the location path is heavy and the ride path must be exact. Next: the boxes. Where the dot lives, how nearby drivers are found, and how a ride flows through them.
Check your understanding
- A candidate stores each driver's latest position in a SQL table, updated on every ping. 50,000 drivers ping every 5 seconds, and one primary handles about 2,000 simple queries per second. What do the numbers say?
- The hot index needs about 5 MB for 50,000 drivers. A teammate says that is tiny, so the location part of the design is easy. What is wrong with that?
- A rider's phone loses signal after tapping Book, so the app retries POST /rides twice. Without any protection, what happens?
- Compare 10,000 location writes per second with 120 ride requests per second. Which statement describes the two paths best?