Appearance
9.7.21 — The Marketplace Family: Food Delivery and Ride Sharing
"Design a food delivery app." "Design a ride hailing service."
Both prompts are the same two problems stacked on top of each other, and candidates almost always answer only the first.
Problem one is matching: find a supplier who is moving, offer them the job, and get an answer in seconds. This is the part everybody prepares — geospatial indexes and nearest-driver queries.
Problem two is orchestration: run a job for the next forty minutes involving three or four independent parties, any of whom can disappear, refuse, or be late, and still end in a defensible state where the right people were charged the right amounts. This is the part that separates answers, and it is where the two prompts genuinely differ.
1. The shared skeleton, and the one question that separates them
| Piece | What it is |
|---|---|
| Request | What the customer wants and where |
| Supply | Couriers or drivers, moving, with a state |
| Offer | A job proposed to one supplier, with a deadline |
| Job | The multi-party state machine that follows acceptance |
| Pricing | An estimate up front and a final amount |
The question that separates the two prompts: how many suppliers must be coordinated?
Ride sharing has one. A driver collects a person and delivers them. The passenger is at the pickup point because they asked to be, and they are in the car for the whole journey. One timeline.
Food delivery has two, and they are independent. A restaurant cooks on its own schedule, and a courier travels on theirs. Neither controls the other, and the job only works if the two timelines meet. Send the courier too early and they stand waiting, unpaid, refusing other work. Send them too late and the food is cold, which is the single most common complaint these businesses receive.
That difference — one timeline or two that must be made to meet — explains most of what follows.
2. Finding nearby supply
A driver's position changes every few seconds, so the location store takes an enormous write rate and a heavy read rate, and every read is "who is near this point".
A plain query does not work, and it is worth knowing why rather than just knowing it is slow. Filtering on latitude between two values and longitude between two values uses one index well and the other barely, because a standard index orders on one dimension first — so the database narrows to a band across the whole world and then scans it. And distance itself cannot be indexed at all, because it is computed from the query point.
The standard fix is to turn two dimensions into one. Cut the world into cells, give every cell a short string identifier, and store each driver's cell.
typescript
interface DriverLocation {
driverId: DriverId;
cell: CellId; // (1) "gcpvj0" — a small square of the world
lat: number; lng: number; // (2) exact position, for the final sort
updatedAt: Instant;
state: DriverState; // (3)
}
type DriverState =
| { kind: "offline" }
| { kind: "available" }
| { kind: "offered"; jobId: JobId; until: Instant } // (4)
| { kind: "onJob"; jobId: JobId };(1) A geohash is the usual encoding: the world is halved repeatedly, alternating between longitude and latitude, and each halving adds a character. Longer string, smaller square. Its useful property is that nearby places usually share a prefix, so "everyone in this area" becomes a prefix match — something a normal index handles perfectly.
(2) The exact position is still stored, because the cell only narrows candidates. The real distance is computed on the few dozen that survive.
(3) The driver's state lives with their location because every dispatch decision needs both, and splitting them means two lookups and a chance of disagreement.
(4) Being offered a job is a state with a deadline, not a boolean. That is what section 3 rests on.
The trap that always gets asked: someone standing near a cell boundary. The nearest driver may be twenty metres away in the next cell, so searching one cell finds a worse driver and misses the best one. Search the cell and all its neighbours — nine cells for a square grid — then compute real distances and sort. Knowing that the edge case exists is the point; it is invisible in testing and obvious to anyone who has run one of these.
Location writes are the highest-volume thing in the system, and they do not deserve durability. A position from eight seconds ago is worthless, so the newest write simply replaces the old one, in memory, with no history. Persisting every position of every driver produces an enormous, useless dataset. The exception worth naming: positions during a job are kept, because they are the evidence in a dispute about a route or an arrival.
3. Dispatch is an offer with a deadline, not an assignment
Once candidates are found, the job has to reach a driver, and the two obvious designs are both wrong in interesting ways.
Broadcast to everyone nearby and let them race. Ten drivers get a notification, one accepts, nine are annoyed. It generates a burst of concurrent claims on one job, the fastest phone wins rather than the best match, and drivers learn to accept everything and cancel later. It fails on fairness and on the quality of the match, not on correctness.
Assign directly to the best driver. Now a driver who has stopped for lunch without going offline receives a job they never respond to, and the customer waits with no idea that nothing is happening.
The design that works is a sequential offer with a short deadline:
typescript
async function dispatch(job: Job, candidates: readonly DriverId[]): Promise<Assigned | NoSupply> {
for (const driverId of candidates) { // (1)
const offered = await offer(job, driverId, OFFER_TIMEOUT); // (2)
if (offered.kind === "accepted") return claim(job, driverId); // (3)
if (offered.kind === "declined") continue;
if (offered.kind === "timedOut") markUnresponsive(driverId); // (4)
}
return { kind: "noSupply" }; // (5)
}(1) Candidates are ranked, not just nearest — by distance, by direction of travel, by acceptance history, and by how long they have been waiting for work. Fairness to drivers is a ranking input, not an afterthought, because a system that always favours the same drivers loses the rest of its supply.
(2) One offer, one driver, a deadline of ten to twenty seconds. The deadline is what makes an unresponsive driver cost seconds rather than the whole job.
(3) Acceptance still goes through a conditional claim, because two offers can overlap during retries and network delays. The claim is an update of the job conditional on it still being unassigned, and the loser is told the job is gone. Never rely on the offer sequence alone for exclusivity — that is the same lesson as every claim in this chapter.
(4) A timeout is information about the driver, not just about this job. Repeated non-response usually means they walked away, and the system should stop offering rather than keep spending fifteen seconds of customer waiting time on them.
(5) Running out of candidates is a real outcome and needs a designed response: widen the radius, wait and retry, or tell the customer honestly that nobody is available. Silence is the worst option and the one that happens by default.
Offering to two or three at once is the standard compromise when supply is thin and waiting is expensive. It reintroduces the race, which the conditional claim already handles, and it costs driver goodwill because most of them lose. It is a dial between customer wait time and driver experience, and treating it as a dial rather than a constant is the mature answer.
4. Food delivery's real problem: two timelines that must meet
So dispatch time is a decision, not "as soon as possible". The courier should be dispatched at roughly the moment when their travel time equals the remaining cooking time. Send earlier and you buy a waiting courier; send later and you buy cold food.
typescript
function shouldDispatchNow(order: Order, courier: CourierCandidate, now: Instant): boolean {
const foodReadyIn = order.estimatedReadyAt.minus(now); // (1)
const travelTime = estimateTravel(courier.position, order.restaurant); // (2)
return travelTime.gte(foodReadyIn.minus(EARLY_BUFFER)); // (3)
}(1) The kitchen's estimate, which is genuinely uncertain and gets worse when the restaurant is busy.
(2) The courier's travel estimate, also uncertain, and worse in traffic.
(3) A deliberate bias towards arriving slightly early. The buffer is asymmetric on purpose, because the two errors do not cost the same: a courier waiting three minutes is a small, known cost the business can pay for, and cold food is a refund plus a customer who does not come back. When two errors have different costs, the design leans towards the cheaper one, and saying that out loud is the point of this section.
Three consequences follow, and each one is a follow-up question:
The estimate must be revised, not fixed. Kitchens run late. If readiness slips by ten minutes after the courier is dispatched, the courier waits, and either they should be released to another job or they should be paid for waiting. Both are policies, and having one is what matters.
Batching two orders onto one courier is the same decision made harder. It only works when both restaurants and both customers lie roughly along one path and both kitchens finish in a compatible window. It raises courier earnings per hour and risks making the second customer's food late, so it needs a hard rule — usually a maximum added delay for the customer whose order is delivered second.
The restaurant is a party that can refuse. A restaurant that rejects an order after the courier is on the way leaves a job with a courier, no food, and a customer expecting dinner. Rejection therefore has to be an explicit state with a compensation path, not an error.
Ride hailing has none of this, and that is precisely why it is the easier of the two prompts once matching is done. The passenger is at the pickup point, the driver arrives, and there is one timeline to manage.
5. The job as a multi-party state machine
typescript
type JobState =
| { kind: "searching"; since: Instant } // (1)
| { kind: "assigned"; supplierId: SupplierId }
| { kind: "atPickup"; arrivedAt: Instant } // (2)
| { kind: "inTransit"; startedAt: Instant }
| { kind: "delivered"; proof: DeliveryProof } // (3)
| { kind: "cancelled"; by: Party; at: JobState["kind"]; fee: Money } // (4)
| { kind: "failed"; reason: FailureReason }; // (5)(1) Searching is a state with a start time, because how long it has been running determines what happens next — widen the radius, raise the incentive, or give up and tell the customer.
(2) Arrival at pickup is its own state because waiting time is often paid, and paid time must have a recorded start.
(3) Completion carries proof: a code the customer reads out, a photo of the doorstep, a signature. Without it, "it never arrived" has no answer, and the business absorbs every dispute.
(4) Cancellation records who cancelled and from which state, because those two facts decide the money. Cancelling while still searching costs nothing; cancelling after a driver has driven ten minutes to reach you does not.
(5) Failure is separate from cancellation. Nobody accepted, the customer was not there, the restaurant closed — these are outcomes with their own handling, and collapsing them into "cancelled" makes the money unexplainable afterwards.
Every state needs a timeout with a defined action, and this is the section candidates skip.
| State | If it lasts too long |
|---|---|
| searching | Widen, raise incentive, then give up |
| assigned | Reassign; the driver is not moving |
| atPickup | Start paid waiting, then release |
| inTransit | Alert; contact both parties |
| delivered | Auto-settle after the dispute window |
A state machine without timeouts describes only the happy path. In a system where every participant is a human with a phone that can run out of battery, every state must have an answer to "what if this one never ends", and having that table ready is a strong signal.
Cancellation is where the compensation lives, and it is worth a short table because the interviewer will ask:
| Cancelled by | When | Outcome |
|---|---|---|
| Customer | While searching | Free |
| Customer | After assignment | Fee, part to the driver |
| Driver | Before arrival | Reassign, no customer charge |
| Restaurant | Any time | Refund, courier compensated |
The principle behind every row is the same: whoever caused someone else to spend time or money is the one who pays for it. Being able to state the principle rather than only the rows is what makes the answer transferable to the cases nobody listed.
6. Pricing: an estimate and a final amount are different things
The estimate is a promise made before the work. Distance, time, demand, and any surge multiplier at that moment. It must be recorded with the job — not recomputed later — because the customer agreed to that number, and recomputing it after a slow journey is how a business gets a reputation it cannot recover from.
The final amount is computed from what happened. Actual distance, actual time, waiting time, tolls. Whether it may exceed the estimate is a policy with a real trade: honouring the estimate makes the price trustworthy and pushes the cost of bad traffic onto the platform, while charging the true amount is fair to the driver and unpredictable for the customer. Most large services cap the difference, and naming the cap as a business parameter rather than a constant is the right answer.
Surge is a strategy object, per area and per minute, and two properties are worth stating because they are what makes it work or fail:
It must be visible before the customer commits, or the price becomes a trap.
It must change smoothly. A multiplier that jumps from 1.0 to 2.2 in one step makes people wait for it to drop, which reduces demand suddenly and drops it back — an oscillation the system created itself. Smoothing is not a presentation detail; it is what stops the pricing signal fighting the behaviour it is trying to change.
7. What the interviewer will push on
"Find the nearest available driver." Turn two dimensions into one with a cell identifier — a geohash, where nearby places share a prefix, so an area query is a prefix match on an ordinary index. Then the detail that shows you have thought about it: search the neighbouring cells too, because the nearest driver is often just over a boundary, and compute true distances only on the few dozen candidates that survive.
"How does the job reach a driver?" A sequential offer to one ranked driver at a time with a ten-to-twenty-second deadline, not a broadcast and not a direct assignment. Broadcasting rewards the fastest phone and trains drivers to accept everything and cancel later; direct assignment stalls on a driver who has walked away. And acceptance still goes through a conditional claim, because retries and network delays can overlap two offers.
"What is genuinely different about food delivery?" Two independent timelines that must meet. The kitchen cooks on its schedule and the courier travels on theirs, so dispatch time is a decision: send the courier when travel time equals remaining cooking time, biased slightly early because a waiting courier is cheaper than cold food. Ride hailing has one supplier and one timeline, which is why it is the simpler of the two once matching is solved.
"The driver accepts and then stops moving." Every state needs a timeout with a defined action, and this one reassigns. Then give the whole table — searching widens and eventually gives up, at-pickup starts paid waiting and then releases, in-transit alerts a human, delivered auto-settles after the dispute window. A state machine without timeouts is a description of the happy path.
"Who pays when someone cancels?" State the principle rather than reciting rows: whoever caused someone else to spend time or money pays for it. Free while searching, a fee after a driver has been travelling towards you, no customer charge when the driver cancels, and a restaurant rejection means a refund plus courier compensation because the courier did nothing wrong.
"The trip cost more than the estimate." The estimate is recorded with the job because the customer agreed to that number. Whether the final amount may exceed it is policy with a genuine trade — honouring it makes prices trustworthy and puts traffic risk on the platform; charging true cost is fair to the driver and unpredictable for the customer. Most services cap the difference, and the cap is a business parameter.
The thing to volunteer that nobody asks for: proof of completion. A code the customer reads to the courier, a photo at the door, a signature — recorded in the delivered state. Without it, "it never arrived" has no answer and the platform absorbs every dispute, including the fraudulent ones. Candidates design the happy path and the cancellations; designing the evidence is what shows you have thought about a business where both sides can lie.
Recall
- These prompts are two problems: matching moving supply, and orchestrating a long multi-party job. Most answers cover only the first.
- The separating question is how many suppliers must be coordinated. Ride hailing has one timeline; food delivery has two independent timelines that must meet.
- Location search: encode the world into cells so nearby places share a prefix, then search the neighbours too and compute real distance on the survivors.
- Location writes are replace-in-place with no history — except during a job, where the track is evidence.
- Dispatch is a sequential offer with a deadline, not a broadcast and not an assignment. Acceptance still needs a conditional claim.
- A timeout is information about the driver, not only about the job.
- Food delivery's dispatch time is a decision: send when travel time equals remaining cook time, biased early, because a waiting courier is cheaper than cold food. When two errors cost differently, lean towards the cheaper one.
- The job is a state machine where every state has a timeout and a defined action. Without them you have described only the happy path.
- Cancellation records who and from which state, because those two facts decide the money. The principle: whoever cost someone else time or money pays.
- The estimate is recorded, never recomputed. Whether the final may exceed it is a capped policy, not a constant.
- Proof of completion — code, photo, signature — is what makes "it never arrived" answerable.
Self-test: What is the second problem most answers skip? Why search neighbouring cells? Why is dispatch sequential rather than broadcast? When should a courier be sent, and which way is the buffer biased? What two facts decide who pays for a cancellation?
Quiz Bank
FoundationalDesign the matching half: find nearby available drivers and get one assigned, at a rate of thousands of location updates a second.
Start with why the obvious query fails. Filtering latitude between two values and longitude between two values uses one index well and the other barely, because an index orders on its first column first — so the database narrows to a band spanning the world and scans it. And distance cannot be indexed at all, since it depends on the query point.
So collapse two dimensions into one. Divide the world into cells and give each a short string identifier. A geohash halves the world repeatedly, alternating longitude and latitude, adding a character per halving — longer string, smaller square. The useful property is that nearby places usually share a prefix, so "everyone in this area" becomes a prefix match, which an ordinary index serves perfectly.
typescript
interface DriverLocation {
driverId: DriverId;
cell: CellId; // the prefix-matchable square
lat: number; lng: number;// exact, for the final sort
updatedAt: Instant;
state: DriverState; // offline | available | offered | onJob
}The cell narrows; it does not decide. Real distance is computed only on the few dozen candidates that survive the cell filter, and the results are then ranked.
The boundary case is the one interviewers ask about. A customer standing near the edge of a cell has their nearest driver twenty metres away in the next cell. Searching one cell finds a worse driver and misses the best one, and this is invisible in casual testing. So search the cell and all its neighbours — nine for a square grid — then sort by true distance.
Location writes deserve a decision of their own, because they are the highest-volume thing in the system. A position from eight seconds ago is worthless, so the newest write replaces the previous one, in memory, with no history and no durability requirement. Persisting every position of every driver produces an enormous dataset nobody reads. The exception: positions during a job are kept, because the route is evidence when someone disputes a fare or an arrival.
Then assignment, which is not the same as ranking. Candidates are ranked by distance, direction of travel, acceptance history, and how long each driver has been waiting for work — fairness to drivers is a ranking input, because a system that always favours the same drivers loses the rest of its supply.
And the offer is sequential with a deadline: one driver at a time, ten to twenty seconds, then the next. Acceptance goes through a conditional claim on the job still being unassigned, because retries and network delays can leave two offers overlapping. A timeout is also recorded against the driver, since repeated non-response usually means they have walked away and each one costs a customer fifteen seconds of waiting.
Finally, running out of candidates is a real outcome and needs a designed response: widen the radius, wait and retry, or tell the customer honestly. Doing nothing is the default behaviour and the worst of the three.
AppliedDesign food delivery's dispatch timing. When is the courier sent, and what happens when the kitchen runs late?
Name the structural difference first, because the whole answer depends on it. Ride hailing coordinates one supplier: a driver collects a passenger who is already there. Food delivery coordinates two independent suppliers — a kitchen cooking on its own schedule and a courier travelling on theirs — and neither controls the other. The job only works if the two timelines finish at the same moment.
So dispatch time is a decision rather than "immediately".
typescript
function shouldDispatchNow(order, courier, now): boolean {
const foodReadyIn = order.estimatedReadyAt.minus(now);
const travelTime = estimateTravel(courier.position, order.restaurant);
return travelTime.gte(foodReadyIn.minus(EARLY_BUFFER));
}Send the courier when their travel time equals the remaining cooking time. Earlier and they stand at the counter unpaid, refusing other work. Later and the food sits cooling.
The buffer is deliberately asymmetric, and the reason is the general principle worth stating: when two errors cost different amounts, lean towards the cheaper one. A courier waiting three minutes is a small, known cost the business can compensate. Cold food is a refund, a bad rating and a customer who stops ordering. So the bias is towards arriving slightly early, always.
Now the follow-up, which is where the design earns its keep: the kitchen runs late.
Readiness is an estimate that must be revised, not a fixed time. The restaurant's own updates, its historical accuracy for this dish at this hour, and how many orders it currently has are all inputs. A restaurant that is reliably ten minutes optimistic should be modelled as such rather than believed.
A courier already dispatched and now waiting needs a policy, and both options are defensible. Release them to another job and re-dispatch later, which is efficient and risks nobody being available when the food is finally ready. Or pay them for waiting, which is simple, keeps the order safe, and costs money. Most services pay after a threshold, and the important thing is that a policy exists rather than which one is chosen.
The customer must be told. A silently late order generates a support contact; a proactively updated arrival time usually does not. This is a product decision that lands directly in the state machine, because it requires the revised estimate to be an event rather than an internal number.
Two extensions worth volunteering.
Batching two orders onto one courier is this same decision made harder. It only works when both restaurants and both customers lie roughly along one path and both kitchens finish within a compatible window. It raises courier earnings and risks the second customer's food being late, so it needs a hard rule — a maximum added delay for whoever is delivered second.
The restaurant can refuse after acceptance, which leaves a courier en route, no food and a waiting customer. That must be an explicit state with compensation — the customer is refunded, the courier is paid for the trip they made — rather than an error path discovered in production.
InterviewA driver accepts a ride and then does not move. Walk through everything your system does.
The first point is structural: this is not an exception, it is a state that lasted too long. Every state in this job has a timeout and a defined action, and a design that only describes the happy path has no answer to this question at all.
Detection comes from two signals, and neither is the driver telling you.
Position is not changing, and the distance to the pickup point is not decreasing. Location updates are already arriving several times a minute for the matching system, so this needs no new machinery.
The estimated arrival time is not advancing. A driver stuck in traffic still shows a plausible, slowly improving estimate; a driver who has stopped shows one that does not improve at all.
Then the ladder of responses, in order of cost.
Prompt the driver. A notification asking if they are on the way, with a quick way to cancel. Many cases are exactly this — someone accepted and got distracted — and a prompt resolves it in seconds.
Reassign. If the pickup is not going to happen in reasonable time, the job returns to searching and is offered to other candidates. The customer is told the driver has changed, which is far better than silence, and the original driver's acceptance record takes the hit.
Compensate. The customer has now waited longer than promised. Depending on how long, that is an apology, a credit, or a discount — a policy, not a constant.
The correctness detail that matters most: reassignment is a conditional claim. The job moves back to unassigned only if it is still assigned to that driver, and the new assignment only takes effect if the job is still unassigned. Without those conditions, a driver who starts moving at the same moment as the reassignment produces two drivers heading to one pickup, and both will expect to be paid.
And the original driver must be told they no longer have the job, positively rather than by silence. A driver arriving to find the passenger gone is a support contact and a lost driver, and it is entirely avoidable.
Then the whole table, because giving it unprompted is the strong move.
Searching too long — widen the radius, raise the incentive, and eventually tell the customer honestly that nobody is available.
Assigned too long — this case: prompt, then reassign.
At pickup too long — start paid waiting after a threshold, then release the driver and mark the job failed for customer non-appearance, with a fee.
In transit too long — alert a human and contact both parties. This is the state where something serious may be happening, and it is the one state where automation should escalate to a person rather than decide.
Delivered — auto-settle after the dispute window closes, so money does not sit in limbo indefinitely.
The general lesson to close on: in a system whose participants are humans with phones that run out of battery, every state must have an answer to "what if this one never ends". The states are easy to enumerate; the timeouts are what make the design real.
StaffIt is Friday at 7pm and demand is double supply in one part of the city. Design what the system does — and be honest about what cannot be fixed with engineering.
Start by naming the shape of the problem, because it decides what is allowed to help. This is not a capacity problem in the computing sense — the servers are fine. It is a shortage of a physical resource, and no amount of engineering creates couriers. So the goal is not to serve everybody; it is to degrade in a way that is honest, fair and recoverable, and to make the shortage smaller where possible.
What the system can genuinely do, in order of how well it works.
Tell the truth early. A long wait shown before ordering is a customer who decides; a long wait discovered after paying is a refund and a complaint. Accurate, pessimistic estimates during a shortage are worth more than any optimisation, and they reduce demand at the exact moment that helps.
Price to move supply and shape demand. A surge multiplier draws couriers into the area and persuades some customers to wait or cancel. Two properties matter: it must be visible before the customer commits, and it must change smoothly, because a multiplier that jumps in one step makes people wait for it to drop, which drops demand sharply and takes the multiplier with it — an oscillation the system created itself.
Batch more aggressively. When couriers are the scarce thing, combining two nearby orders onto one courier increases effective supply directly. The hard rule stays: a maximum added delay for whoever is delivered second, because the fix must not create a worse failure.
Widen the search and hold the queue. Couriers slightly further away become acceptable when the alternative is nobody, and orders that cannot be matched wait in a queue rather than failing instantly — with the queue's honest position shown, and a genuine cancel option.
Protect the jobs already running. An in-flight delivery is worth more than a new one, because a customer waiting with food already cooked is closer to a bad outcome than one who has not ordered. When the two compete for a courier, the running job wins.
What cannot be fixed, and saying so is the point of the question.
Demand at double supply means half of it will not be served well. There is no dispatch algorithm that changes that arithmetic. What is genuinely available is which half is disappointed and how — and that is a business decision that should be made deliberately rather than falling out of a queue's default behaviour. A system that quietly serves whoever refreshes fastest has made that decision without anyone choosing it.
Two failure modes specific to a shortage, both worth designing against.
The dispatch death spiral. Long searches mean every job holds a courier's attention through several offers, which slows every other job's matching, which lengthens searches further. The defence is a hard cap on how long a job may search before it either escalates its incentive or is told plainly that supply is unavailable — freeing the offer capacity for jobs that can succeed.
Cancellation storms. Customers waiting too long cancel in groups, which releases couriers in a burst, which briefly makes the situation look solved, which drops the surge, which brings demand back. Smoothing the pricing signal and using a slightly delayed measure of supply are what stop the system oscillating against itself.
What I would monitor, since these are the numbers that describe a shortage rather than a fault: unmatched requests as a share of all requests, which is the real supply gap; time from request to assignment at the 95th percentile, since the average hides exactly the people being failed; the offer-to-acceptance ratio, which tells you whether couriers are declining because the pay is wrong rather than because they are absent; and completed jobs per courier per hour, which is the only number that shows whether batching and dispatch changes actually created supply rather than just moving it around.
The closing point worth making explicitly: the best engineering here is honest communication and a fair degradation policy, not a cleverer matching algorithm. An interviewer asking this question is usually checking whether you can tell the difference between a problem you can solve and a problem you can only handle well.
Flashcards
FlashThe two problems
Matching moving supply, and orchestrating a long multi-party job. Most answers cover only matching. The orchestration half is where the two prompts actually differ.
FlashCell search
Encode the world into cells so nearby places share a prefix, making an area query a prefix match. Then search the neighbouring cells too — the nearest driver is often just over a boundary — and compute real distance on the survivors.
FlashWhy offers are sequential
Broadcasting rewards the fastest phone and trains drivers to accept and cancel. Direct assignment stalls on a driver who walked away. One ranked driver, a ten-to-twenty-second deadline, then the next — plus a conditional claim on acceptance.
FlashWhen to dispatch a courier
When travel time equals remaining cook time, biased slightly early. A waiting courier is a small known cost; cold food is a refund and a lost customer. When two errors cost differently, lean towards the cheaper one.
FlashEvery state needs a timeout
Searching widens then gives up; assigned reassigns; at-pickup starts paid waiting then releases; in-transit escalates to a human; delivered auto-settles after the dispute window. Without timeouts you have described only the happy path.
FlashWho pays for a cancellation
Recorded by who cancelled and from which state. The principle: whoever caused someone else to spend time or money pays for it. Free while searching, a fee once a driver is travelling towards you.
Next: 9.7.22 — library lending and course registration, where the design turns on what happens to the eleventh person in a queue of ten.