← Architecture Lab

Driver Dispatch Under Timeouts

A search problem with a clock on it — expanding rings, batched offers, and a retry loop for when the answer is 'not yet' rather than 'no'.

request ridecreate requestRider AppNearby DriversDispatch APIMySQLRedis QueueRetry Worker

Rider App → Dispatch API → MySQL

The request opens a stream

A ride request is created and acknowledged immediately, but the response isn't closed. Dispatch can legitimately take minutes, so the connection stays open and the app is told what actually happens — accepted, queued, or exhausted. The alternative is a single reply and a client left polling an operation whose duration nobody can predict in advance.

Why not the alternatives?

Broadcast to every driver in the city
Fastest possible assignment and a notification storm for a ride one person can take. It also destroys proximity ordering — a driver twenty minutes away competes with one around the corner, and riders wait longer for worse pickups.
Offer to one driver at a time
No wasted notifications, and an unassigned rider watching a spinner while the system works through unresponsive drivers one acceptance window at a time. The batch is the compromise: bounded waste, bounded wait.
Query once, then work through the list
Cheaper in database terms and increasingly wrong. A sweep lasts long enough for drivers to move, go offline, or take other rides, so dispatching from a snapshot means dispatching to where drivers used to be.
Treat an exhausted sweep as failure
Simple, and it discards the distinction the system exists to preserve. Nothing about a failed sweep says the next one fails too, and availability turns over on a timescale shorter than a rider's patience.
Let the queue's automatic retry handle re-attempts
Automatic retry replays a job that errored. A dispatch re-attempt isn't an error — the previous sweep completed correctly and found nobody — and it must re-validate that the request still exists before acting. Conflating the two means retrying rides that were already cancelled.

Lessons

  • 'Not found' and 'not yet' are different answers. Most of the value in this pipeline came from refusing to collapse them into one.
  • Freshness beats efficiency when the underlying data is in motion — re-querying every ring looked wasteful and was the difference between dispatching to drivers and dispatching to their last known positions.
  • An operation that legitimately takes minutes isn't a slow request; it's a process with progress. Give it a channel to say so, or the client invents polling and guesswork.
  • The failure path holds the money. Releasing funds correctly across every exit is harder, and matters more, than the assignment logic everyone focuses on.
  • Logic that runs in two execution contexts will drift apart. Shared behaviour has to be shared code — a copy with a comment explaining that it's a copy is a bug with a delay on it.