Free preview

Workflow and High-Level Design

In one line: step six contains a loop, and loops in a user-facing flow have latency budgets. That is the only genuinely hard thing in the workflow, and the design states it in one line.

The workflow

  1. When the rider opens the application, they see all nearby available drivers.
  2. The rider enters a destination and requests a ride.
  3. The system receives the request and searches for a suitable driver.
  4. The rider's status remains "Waiting for driver" until a match is confirmed.
  5. Drivers transmit their location every four seconds. The system uses this data to track availability and route progress.
  6. The matched driver accepts or rejects the request:
  • Accept: the system updates the status for both parties. The rider receives the driver's details and confirmation.
  • Reject: the system automatically searches for the next available driver.

The rejection loop is the hard part, and it is one sentence in the design

"Reject: the system automatically searches for the next available driver."

That single line hides the workflow's only real engineering problem, because it makes step 6 a loop with no stated bound sitting inside a user-facing wait.

Three things it does not say:

How long do you wait for a response? A driver who does not answer is indistinguishable from one who is driving, has poor signal, or has put the phone down. So there must be a timeout — typically 10 to 20 seconds — and a timeout is a rejection.

How many drivers do you try? Each attempt costs its timeout. Three sequential attempts at 15 seconds is 45 seconds of a rider staring at "Waiting for driver," which is far past what people tolerate.

Do you ask them one at a time? Sequential offers are fair and slow. Broadcasting to several drivers at once is fast and creates a race — two drivers accept, one is disappointed, and now you have a distributed-consensus problem over a physical commitment.

The real trade is latency against wasted offers, and production systems sit in between: offer to a small batch, stagger the offers by a second or two so the best-ranked driver has a head start, and expand the radius as the search continues.

Note also the interaction with Lesson 3's numbers. The driver's location is up to 15 seconds stale, so during a 45-second matching sequence the candidate set is drifting. A driver who was nearest when the search began may not be by the time they are asked.

Any loop in a user-facing flow needs a bound, and the bound is a product decision, not an implementation detail. Naming the timeout, the retry count, and the fallback is what separates a workflow you have thought about from one you have transcribed.

Step 1 is a much heavier query than it looks

"When the rider opens the application, they see all nearby available drivers."

This happens before any ride is requested — on every app open, from 20 million daily active riders, including people who are just checking whether cars are around and will never book.

So the proximity query runs far more often than trips occur. And it is not a one-shot query either: those little cars move on the rider's screen, so the client is subscribed to a stream of position updates for drivers in view.

Two consequences:

It is a read amplifier on the most write-loaded component. The quadtree is absorbing 750,000 writes per second and simultaneously answering browse queries for riders who may never convert.

It argues for tolerating stale data at exactly this step. Nobody is harmed if a displayed car is 15 seconds behind — it is ambient information, not a decision. This is the clearest case of Lesson 1's principle: the index feeds a candidate set, not an answer.

A sensible refinement the design does not mention: cap the number of drivers returned for browsing. A rider in Manhattan does not need all 400 nearby cars, and returning the nearest 20 turns an unbounded response into a bounded one.

The high-level design

WebSockets are load-bearing here in a way they were not in earlier chapters

The Google Maps chapter used WebSockets for navigation telemetry — a one-way stream from client to server.

Here the connection is bidirectional and essential in both directions:

DirectionCarries
Client to serverDriver location, every 4 seconds
Server to clientRide offers, trip state changes, arrival alerts

The server-to-client direction is what makes this non-negotiable. A ride offer is server-initiated and time-critical — a driver has seconds to respond. Polling would add latency to exactly the step Lesson 4's rejection loop cannot afford, and push notifications are best-effort with delivery latency measured in seconds.

That gives the sizing unit for this tier: concurrent connections, not requests per second. With 20 million riders and 3 million drivers potentially connected, this is the C10K shape from the YouTube, Maps, and Quora chapters, and it argues again for event-driven servers over thread-per-connection.

It also creates a routing problem worth naming: a ride offer must reach one specific driver's connection, which lives on one specific server. So the system needs a connection registry — driver ID to server — consulted on every offer. The design does not show one, and it is a real gap. It is the same role ZooKeeper played in earlier chapters, and without it the system cannot push anything to anyone.

Services split by lifetime, not just by function

The component list looks like ordinary microservice decomposition. But notice they differ in how long their work lasts, which is why they are separate:

ServiceLifetime of one unit of work
Location manager4 seconds, forever, per driver
Request vehicle / find driverSeconds — a matching attempt
Trip managerTens of minutes — a whole trip
Payment serviceDays — settlement clears long after

A single service handling all four would be sized, scaled, and failed over on four incompatible schedules. The location manager needs to absorb a firehose; the trip manager needs to hold long-lived state consistently; the payment service needs durability across days.

When components differ by orders of magnitude in the lifetime of their work, they belong in separate services — the same argument that building block made for splitting the route finder from the navigator, and that building block made for putting billing behind pub-sub.

Key takeaway

Step 6's rejection loop is the workflow's only hard part and gets one sentence: it needs a timeout (a non-answer is a rejection), a retry bound, and a decision between sequential offers and broadcasting, which is fast but races two drivers into one physical commitment. Step 1 — showing nearby cars on app open — is a read amplifier on the most write-loaded component, and it should tolerate 15-second staleness precisely because it is ambient rather than a decision. WebSockets are bidirectional and essential, because ride offers are server-initiated and time-critical, which implies a connection registry the design never shows. And the services split by lifetime of work — 4 seconds, seconds, tens of minutes, days.

Next: the API, and what its parameters give away.

Enjoying the preview?

Create a free account to unlock the rest of this course, the in-browser judge, and live AI mock interviews.

Sign up free to continue