Technologies referenced in this case study: Redis · Apache Kafka · Cassandra · PostgreSQL · Flink & Stream Processing · ZooKeeper & etcd
Related case studies: Maps & Geospatial · Payment Processing · Real-Time Updates · Stream Processing · Proximity Matching · Notification System
How to Use This Case Study#
Organized for interview use first, reference second. Read once end to end, then drill the sections you're weakest on.
| Mode | Time | What to Read |
|---|---|---|
| Quick Review | 15 min | Executive Summary → Interview Walkthrough → Fault Lines table → Active Drills 1–3 |
| Targeted Study | 1–2 hrs | Executive Summary → Walkthrough → Section 3 (Fault Lines) → Section 4 (Failures) → Deep Dives 1 and 4 |
| Deep Dive | 3+ hrs | Everything, including the Principal Lens and appendices |
What is Ride Hailing & Delivery? — Why interviewers pick this topic
A real-time, two-sided logistics marketplace: riders (or eaters, or shippers) create demand at a point; drivers (or couriers) are mobile supply that must be assigned — not chosen — to that demand within seconds, tracked continuously, priced dynamically, and paid reliably. Uber, Lyft, DoorDash, Grab and Instacart are all variations.
Before vs After — New Year's Eve, 00:05:
Naive design (nearest-driver query, DB row per trip, synchronous payment):
t=0: Midnight. Trip requests in one city go 40/s → 600/s in 90 seconds.
t=+10s: Every request runs "SELECT nearest drivers" against the location table,
which is also taking 60K GPS writes/s. Lock contention on hot rows.
t=+30s: Two requests are offered the same driver. Both riders see "Driver on the way."
t=+2min: Card authorizations to the payment processor time out at 3s. Trip creation
blocks on payment. Request p99 goes from 400ms to 12s. Riders mash retry.
t=+5min: Duplicate trips, stranded riders, drivers with two pickups. Support queue x40.
Staff design (in-memory location per city cell, leased offers, async payment saga):
t=0: Same spike.
t=+10s: GPS writes land in an in-memory, city-sharded index; durable history is async.
t=+30s: A driver can hold exactly one offer lease (TTL 15s). The second request
sees the lease and moves to the next candidate.
t=+2min: Payment pre-auth runs asynchronously with a risk-based fallback; the trip
proceeds. Surge rises in hot hexes within ~1–2 min and pulls drivers in.
t=+5min: Completion rate 92% vs 97% baseline. Degraded, not broken. A metric blip.
Why interviewers reach for this question: it has every Staff ingredient in one prompt — a firehose write path (GPS), a contention problem (one driver, many requests), a long-running state machine that crosses money (trip → payment), a control loop that shapes behavior (surge), and a hard multi-region story (a trip cannot "fail over" by being forgotten). Candidates who answer it as "a nearby-drivers query" get leveled Senior.
Mechanics Refresher: Dispatch Strategies
| Strategy | How It Works | Pros | Cons |
|---|---|---|---|
| Greedy nearest | Each request immediately offered to the closest available driver (by ETA, not distance) | Lowest wait-to-offer latency; trivially parallel | Locally optimal, globally wasteful; steals drivers from requests 5 s later |
| Batched matching | Collect requests and drivers for 2–10 s per zone, solve an assignment (Hungarian / min-cost flow) | 5–15% lower total pickup ETA in dense areas (industry-reported range) | Adds the batch window to every request; single solver per zone |
| Broadcast / first-accept | Offer to N drivers, first to accept wins | Fast acceptance in sparse areas | Driver frustration; race on accept; unfair to slower phones |
| Sequential offers with lease | Offer to one driver at a time with a 10–15 s timeout | Clear single ownership of each offer | Slow when acceptance rates are low |
| Dispatch + chaining | Assign a driver who is finishing a trip nearby | Reduces idle time | ETA uncertainty; rider sees a longer "arriving" |
For most production systems: ETA-ranked candidates, batched assignment in dense zones (2–5 s window), greedy in sparse zones, and a sequential offer lease so a driver never holds two offers. The algorithm is rarely the interview question — the location firehose, offer contention, and the trip-to-payment state machine are.
Executive Summary
If you only read one section, read this. Everything else in the case study elaborates on the contrast and the positions below.
What This Interview Actually Tests#
Uber is not a "find the nearest driver" question. A geo index answers that in a millisecond.
It is a real-time control loop over mobile supply, wrapped around a durable state machine that moves money — and the design is about keeping the high-rate, lossy parts (location) from contaminating the low-rate, exact parts (assignment, trip state, payment). It tests:
- Whether you see that location ingestion (~250K writes/s at 1M online drivers) dwarfs trip requests (~1K/s at peak) by ~250×, and design each path for its own consistency needs
- Whether you prevent double-assignment of a driver structurally, not with a retry
- Whether you model the trip as an explicit state machine with a single owner, idempotent transitions and a payment saga
- Whether you treat surge as a marketplace control system with feedback, guardrails and regulatory owners — not a multiplier column
The key insight: Location is disposable — the next ping arrives in 4 seconds, so losing one costs nothing. A trip transition is not — losing "driver accepted" or "trip completed" strands a person or loses money. Staff engineers give these two data classes completely different storage, replication and failure behavior.
The L5 vs L6 vs L7 Contrast — Start Here#
| Behavior | Senior (L5) | Staff (L6) | Principal (L7) |
|---|---|---|---|
| First move | Draws rider app → API → "Matching Service" → DB | Separates the location firehose from the trip state machine and sizes each | Asks which capabilities (location, dispatch, pricing, payments) are shared across rides, eats, freight — and who owns each platform |
| Location | Writes every GPS ping to a database table | Latest location in memory per city shard; history streamed async to cold storage; drops are acceptable | Prices the ingestion fleet and history retention; sets org-wide location-data retention and privacy policy |
| Dispatch | Nearest available driver, update status to busy | ETA-ranked candidates, batched assignment in dense zones, offer lease with TTL so a driver holds one offer | Treats dispatch as a marketplace policy engine with experiment governance and driver-fairness metrics |
| Trip state | A status column updated by whichever service | Explicit state machine, single owner per trip, versioned transitions, outbox events | Defines the trip lifecycle as the contract that 20+ downstream teams consume; versioned schema, deprecation policy |
| Payments | Charge the card when the trip ends | Pre-auth at request, capture at completion, idempotency keys, saga with compensations; trip never blocks on payment | Owns the risk policy: who absorbs failed charges, fraud loss budget, driver-payout guarantees |
| Surge | Multiplier = demand / supply | Per-hex control loop, smoothing, caps, quote locking with TTL | Surge as regulated pricing: emergency caps, audit trail, public commitments, legal sign-off |
Why "first move" separates levels
L5: Draws the request path first — rider taps, service finds a driver, writes a trip. Everything shares one database. This is a coherent design that collapses under GPS write load: 1M online drivers × 1 ping / 4 s = 250K writes/s into the same store that must do transactional trip updates.
L6: "There are two very different data flows here. Location: ~250K writes/s, each one superseded 4 seconds later, so losing one is free. Trips: ~1K creates/s at peak, but every transition must be exact. I'll build them as separate systems: an in-memory, city-sharded location index, and a durable trip state machine. Dispatch reads from the first and writes to the second."
L7: "Rides, delivery and freight all need location ingestion, ETA and dispatch. I'd want one location platform and one ETA service with product-owned dispatch policies on top, because the ETA model is the single most expensive shared asset and divergent copies will disagree with each other in front of customers."
Why "dispatch" separates levels
L5: "Find the nearest available driver and set their status to busy." Two concurrent requests can read the same "available" driver before either write lands. Adding a transaction on a global drivers table makes it correct and slow.
L6: "Candidates are ranked by ETA, not straight-line distance — a driver 300 m away across a highway can be 8 minutes out. Offering is a lease: SET driver:{id}:offer = trip_id NX PX 15000 on the city shard. A driver holds at most one offer; if they don't accept within 15 s the lease expires and we move on. In dense zones I batch requests for ~2–3 s and solve the assignment jointly."
L7: Recognizes that dispatch objective choices (minimize rider wait vs driver deadhead vs fairness of trip distribution across drivers) are marketplace policy with labor-relations implications, and sets up the governance for changing them.
Why "payments" separates levels
L5: "At trip end, call the payment service to charge the card." If the charge fails or times out, what state is the trip in? Is the driver paid?
L6: "The trip completes regardless of payment. Payment is a saga keyed by trip ID: pre-authorize an estimate plus buffer at request time, capture the final fare at completion with idempotency key trip_id:capture, and if capture fails, retry, then fall back to a secondary method, then to arrears on the rider account. The driver's earnings are credited on completion — the business absorbs collection risk, not the driver."
L7: Sets the risk envelope: acceptable fraud-loss basis points, when to require pre-auth vs allow trusted riders to skip it, and how payouts are guaranteed in markets with different payment rails — owned jointly by Payments, Risk and Finance.
The Staff Positions#
| Position | Rationale |
|---|---|
| Location is ephemeral; trips are durable | Keep latest location in memory (replicated or not), history async; trips go to a durable, strongly consistent store |
| Shard everything by city (or region cell) | Trips are local; a city is the natural failure and scaling unit; cross-city queries are rare |
| ETA, not distance, ranks candidates | Road network and traffic dominate; straight-line distance misranks ~20–30% of candidates in cities |
| A driver holds at most one offer — enforced by lease | Structural prevention of double-assignment beats detect-and-apologize |
| Trip is a state machine with one owner | Versioned transitions, idempotent commands, events via outbox; no service writes status directly |
| Payment never blocks the trip | Pre-auth at request, capture async; collection risk is a business decision, not a request timeout |
| Surge is a smoothed control loop with caps | Unsmoothed multipliers oscillate; caps and quote locks are regulatory and trust requirements |
The Three Intents#
| Intent | Constraint | Strategy | Failure Mode | Correctness Bar |
|---|---|---|---|---|
| On-demand ride hailing (one rider, one driver, now) | Time-to-match < ~30 s; pickup ETA | In-memory location, ETA-ranked dispatch with leases, trip state machine | Double assignment, stranded rider, surge oscillation | Exactly one driver per trip; every transition durable |
| On-demand delivery (eater, restaurant, courier) | Food readiness; three-sided timing; batching multiple orders | Dispatch timed to prep-time predictions; order batching; courier assignment can be delayed deliberately | Cold food, courier idle at restaurant, merchant overload | Exactly one courier per order; ETA accuracy is the product |
| Scheduled logistics (freight, grocery windows, reservations) | Cost per stop; hours of planning horizon | Vehicle routing optimization (VRP) in batch; manifests | Infeasible routes, missed windows | Plan feasibility; changes via replanning |
🎯 Staff Move: "I'll design on-demand ride hailing, because it has the tightest latency, the sharpest contention on a single driver, and the most direct money path. Then I'll show what changes for delivery — mainly that the right time to dispatch isn't 'now', it's 'when the food will be ready minus the courier's travel time'. Scheduled logistics is a batch optimization problem and I'd scope it out."
The Five Fault Lines#
| # | Fault Line | The Tension |
|---|---|---|
| 1 | Location Freshness vs Ingestion Cost | Ping every 1 s for accuracy, or every 4–5 s for battery, bandwidth and 4–5× cheaper ingestion? Durable or in-memory? |
| 2 | Greedy vs Batched Dispatch | Offer immediately (lowest latency) or wait 2–5 s to optimize globally (lower total ETA)? |
| 3 | Assignment Consistency: Lease vs Transaction vs Detect-and-Repair | How do you guarantee a driver is never assigned twice without a global lock? |
| 4 | Surge: Market Efficiency vs Fairness and Stability | Price fast enough to clear the market without oscillation, gouging headlines or regulatory breach |
| 5 | Trip State Durability vs Availability (and the Payment Handoff) | A trip must survive regional failure; payment must be exact; neither may block the rider |
In the Wild: Real Production Systems#
Why this section belongs here: these are publicly documented, and citing them shows you know how the real systems evolved.
Uber — H3 and Ringpop-Era Dispatch#
Uber open-sourced H3, a hexagonal hierarchical spatial index, and has described using it for pricing and marketplace analysis: hexagons have uniform neighbor distances, which makes supply/demand aggregation and smoothing across neighbors clean. Its earlier dispatch system (publicly described around 2015–2016) sharded state across nodes using Ringpop, a consistent-hashing library with SWIM gossip membership, so each node owned a slice of the geo space.
Staff insight: The shard key for dispatch is geography because the workload is geographic — a request only ever needs drivers within a few kilometers. Say "city/cell is the unit of sharding and of failure" and you've made the most important architecture decision in one sentence.
Uber — Fulfillment Platform Re-architecture on Spanner#
Uber's engineering blog described rebuilding its fulfillment platform — the system that models trips, orders and the entities that fulfill them — onto Google Cloud Spanner to get strongly consistent, transactional updates across entities while scaling horizontally, replacing an earlier design that relied on in-memory state with application-level consistency.
Staff insight: At some scale, "eventually consistent trip state + reconciliation" costs more in engineering and incident time than paying for a strongly consistent store. The trip/order state machine is the one place a transactional store is worth its price.
DoorDash — Dispatch Timed to Food Readiness#
DoorDash has written publicly about its dispatch system (known internally as DeepRed), which uses ML predictions for food prep time and travel time and solves assignment as an optimization problem, including deciding when to dispatch and whether to batch multiple orders onto one courier.
Staff insight: Delivery flips the objective. For rides, dispatching instantly is almost always right. For food, dispatching too early wastes courier time at the restaurant and dispatching too late gives cold food. Mentioning this shows you've understood intents, not just architectures.
What Interviewers Probe#
| After You Say... | They Will Ask... | (What They're Evaluating) |
|---|---|---|
| "Drivers send their location every few seconds" | "How many writes per second? Where do they go? What if you lose some?" | Sizing and data classification |
| "Find the nearest driver" | "Nearest by what? Two riders request at once — can they get the same driver?" | ETA awareness, contention |
| "Update the trip status" | "Who is allowed to update it? What if the driver app and the rider app disagree?" | State machine ownership |
| "Charge the card at the end" | "The charge times out. Is the trip complete? Is the driver paid?" | Saga, idempotency, who absorbs risk |
| "Surge = demand/supply" | "It flips between 1.0× and 3.0× every minute. Why, and how do you fix it?" | Control-loop thinking |
| "Multi-region for availability" | "A region dies mid-trip. What happens to the 200K active trips?" | Durable state placement |
System Architecture Overview#
Reading the diagram: three planes with three different consistency contracts. The location plane is in-memory and lossy on purpose — a lost ping is replaced in 4 s. The dispatch plane is per-city and uses short leases so a driver holds one offer at a time. The trip plane is the only durable, strongly consistent store; every transition writes to it and emits an event through the outbox. Payment and surge are consumers of events, never synchronous dependencies of the rider's request.
Quick-Reference: The 30-Second Cheat Sheet#
| Topic | The L5 Answer | The L6 Answer — Say This |
|---|---|---|
| Location | "Write GPS to the drivers table" | "Latest position in memory, sharded by city; history async to object storage. A lost ping is free." |
| Nearby search | "Geohash query for nearest drivers" | "H3 k-ring around the pickup for candidates, then rank by ETA from the routing service." |
| Assignment | "Mark driver busy in a transaction" | "Offer lease SET NX PX 15000 per driver; one offer at a time; batch in dense zones." |
| Trip | "Status column" | "State machine, single owner, versioned transitions, outbox events." |
| Payment | "Charge at end" | "Pre-auth at request, capture at completion, idempotency key per step, trip never blocks on payment." |
| Surge | "demand / supply" | "Per-hex control loop every ~60–120 s, smoothed across neighbors, capped, quote locked for a few minutes." |
| Multi-region | "Active-active everything" | "City pinned to a home region; trip store replicated synchronously within region, async across; location rebuilt from pings in 4 s after failover." |
Key Numbers Worth Memorizing#
| Metric | Value | Why It Matters |
|---|---|---|
| Driver GPS ping interval | ~4 s (commonly cited for Uber's driver app) | Sets ingestion rate and freshness bound |
| Online drivers at global peak (assumption) | ~1M | → ~250K location writes/s |
| Ping payload | ~100–200 bytes | ~25–50 MB/s ingest; ~2–4 TB/day of raw history |
| Trips per day (Uber-scale, public order of magnitude incl. delivery) | ~25–30M | ~300/s average, ~1K/s peak, ~5–10× spikes on New Year's Eve |
| Location : trip-request write ratio | ~250 : 1 | Why they must be separate systems |
| Offer timeout / lease TTL | 10–15 s | Bounds how long a driver is "held" |
| Batch window (dense zones) | 2–5 s | Adds latency; buys global efficiency |
| H3 res 8 / res 9 hex | ~0.74 km² / ~0.11 km² (edge ~460 m / ~175 m) | Candidate search and surge granularity |
| GPS accuracy | ~5–10 m open sky; 30–100 m+ in urban canyons | Map-matching needed; don't trust raw points for fares |
| Surge recompute cadence | ~1–2 min per hex | Faster oscillates; slower misses the spike |
| Price quote lock | ~2–5 min | Honoring what the rider saw |
| Trip request → driver assigned p50 | < ~10–30 s in healthy markets | The product's first promise |
| Pre-auth hold | estimate + ~10–20% buffer | Covers route deviations without re-auth |
Interview Walkthrough
The most common mistake: candidates spend 15 minutes on geohash vs quadtree for "nearby drivers" and never reach the trip state machine or payments. The geo index is two sentences. The phases below keep it that way.
Phase 1: Requirements & Framing (2–3 min)#
Functional core in one breath:
"Riders request a ride from A to B, see a price, get matched to a nearby driver, track the driver, take the trip, and pay. Drivers go online, stream location, receive offers, accept, and get paid."
Then the non-functionals, with numbers:
"Let me size the two flows separately. Assume ~1M drivers online at peak, pinging every 4 seconds — that's ~250K location writes/s. Trips are ~25M/day, ~1K requests/s at peak, with New Year's-style spikes of 5–10× in a single city. So location is a ~250:1 firehose relative to trips. Location is disposable — the next ping replaces it. Trip transitions are not — they're money and safety."
Commit to the intent and constraints:
"I'll design on-demand rides. Targets: request-to-assignment p50 under ~15 s, a driver is never assigned to two trips, every trip transition is durable, payment never blocks the ride. I'll come back to what changes for food delivery."
🎯 Staff Move: Naming the 250:1 ratio in the first two minutes tells the interviewer you won't put GPS pings in the trip database — and buys you permission to skip the naive design entirely.
Phase 2: Core Entities & API (1–2 min)#
- DriverLocation:
driver_id,lat/lng,heading,speed,ts,h3_cell— latest only, in memory - DriverState:
driver_id,status ∈ {offline, available, offered, en_route, on_trip},current_trip_id,vehicle_class - Trip:
trip_id,rider_id,driver_id?,state,version,pickup,dropoff,quote_id,fare, timestamps per transition - Quote:
quote_id,price,surge_multiplier,expires_at(~2–5 min) - PaymentIntent:
trip_id,auth_id,amount_authorized,amount_captured,state
POST /v1/quotes {pickup, dropoff, product} → {quote_id, price, surge, expires_at}
POST /v1/trips {quote_id, idempotency_key} → {trip_id, state: MATCHING}
GET /v1/trips/{id} → {state, driver, eta, version}
POST /v1/trips/{id}/cancel {version} → {state, cancel_fee?}
Driver socket (bidirectional, persistent):
← location {lat, lng, heading, speed, ts} every ~4 s
→ offer {trip_id, pickup, eta, expires_in: 15}
← offer_response {trip_id, accept|decline}
← trip_action {trip_id, action: arrived|start|complete, version}
🎯 Staff Move: "Every trip mutation carries a
version, and creation carries an idempotency key. Mobile clients retry constantly — a rider double-tapping 'Request' in a tunnel must create one trip, and a driver tapping 'Complete' twice must complete it once."
Phase 3: High-Level Architecture (≤ 5 min)#
Three sentences:
"Location ingest writes latest positions to an in-memory index sharded by city — no database on that path. A trip request creates a durable trip in MATCHING and asks the city's dispatcher for a driver; dispatch pulls H3-ring candidates, ranks by ETA, and offers under a 15-second lease. Every trip transition commits to the trip store with an outbox row, and payment, surge, receipts and analytics consume those events asynchronously."
Phase 4: Transition to Depth#
"The nearby search is an H3 k-ring lookup — I don't think that's where the risk is. The three places I'd want to go deep are: preventing double-assignment under a request spike, the trip state machine and how it hands off to payments, and surge as a control loop. I'd suggest starting with double-assignment since it's the failure riders see first."
Phase 5: Deep Dives (25–30 min)#
| Deep Dive | Time | What You Must Land |
|---|---|---|
| Dispatch & double-assignment | 7–9 min | ETA ranking, lease per driver, batching in dense zones, accept path is a conditional state transition |
| Trip state machine & payment handoff | 7–9 min | States, single owner, versioned transitions, outbox, pre-auth/capture saga, who absorbs failure |
| Location ingestion | 4–5 min | In-memory latest, async history, city sharding, reconnection storms |
| Surge | 4–6 min | Per-hex supply/demand, smoothing, caps, quote lock |
| Multi-region | 3–4 min | City home region, trip store replication, location rebuild |
Double-assignment, as you'd say it:
"Two requests 200 ms apart in the same block both see driver D as the best candidate. Dispatch for each does SET offer:D trip_1 NX PX 15000 on the city's lease store; only one succeeds. The loser takes its next candidate. When D accepts, the Trip Service does a conditional transition: trip_1 from MATCHING to DRIVER_ASSIGNED if the version matches and the lease still names trip_1. If D accepts after the lease expired and trip_1 was reassigned, the accept is rejected and D sees 'offer expired'. At no point can D hold two trips."
Phase 6: Wrap-Up (2–3 min)#
"To summarize: location is an in-memory, lossy, city-sharded firehose; dispatch holds one lease per driver; the trip is a durable state machine with an outbox; payment is a saga that never blocks the ride. What I'd monitor first: double-assignment count (should be zero), assignment latency p50/p99 per city, and payment capture failure rate. What I'd build next: batched matching in the top 20 cities and delivery-specific dispatch timing. What I skipped: route optimization for scheduled logistics and the ETA model itself."
Common Timing Mistakes#
| Mistake | Time Lost | Fix |
|---|---|---|
| Designing a geo index from scratch | 8–12 min | "H3 k-ring, in memory, per city." |
| Designing maps / routing | 10+ min | "ETA is a service with a road graph and live traffic; I'll treat it as a dependency." |
| Rider and driver apps' UI flows | 5 min | Name the state machine instead; the UI renders states |
| Payment processor internals | 8 min | "Payment is its own design — here's the handoff contract." See Payment Processing |
| No numbers until minute 20 | Whole interview | 250K/s vs 1K/s in Phase 1 |
1. The Staff Lens#
1.1 Why This Problem Exists in Staff Interviews#
Ride hailing is the canonical "mixed-criticality" system: the same user action touches data that can be lost freely and data that can never be lost. Interviewers use it to see whether you classify data before storing it.
| Data Class | Rate | Loss Tolerance | Consistency | Storage | Who Notices Loss |
|---|---|---|---|---|---|
| Driver location | ~250K/s | High (next ping in 4 s) | None needed | In-memory, per city | Nobody |
| Offer / lease | ~1–5K/s | Medium (expires in 15 s) | Single-key atomic | In-memory with TTL | Driver (missed offer) |
| Trip transitions | ~5–10K/s | Zero | Linearizable per trip | Durable, replicated | Rider, driver, finance |
| Payment steps | ~1–2K/s | Zero, and idempotent | Exactly-once effect | Payment ledger | Finance, auditors |
| Surge multipliers | ~1 per hex per min | Medium (stale by 1–2 min) | Versioned snapshots | Cache + log | Rider (price), regulator |
A Senior design that puts all five in one database is overpaying for location and underprotecting payments at the same time.
1.2 The L5 vs L6 Contrast — Visual#
1.3 The Staff Question That Cuts Through Everything#
"If we lose this write, what happens to a person?"
- Lose a GPS ping → the map dot jumps 4 s later. Nothing happens. Keep it in memory.
- Lose an offer → driver misses one request; it's re-offered. Short TTL, no durability.
- Lose "driver accepted" → rider waits for a driver who thinks they're assigned, or two drivers show up. Durable, conditional.
- Lose "trip completed" → rider isn't charged, driver isn't paid, driver can't take the next trip. Durable, idempotent, outbox.
🎯 Staff Move: "I sort every write by what happens to a person if it's lost. That sort gives me three storage tiers, and it's the reason GPS pings never touch the trip database."
2. Problem Framing & Intent#
2.1 The Three Intents — Explained#
Intent 1 — On-demand ride hailing. One rider, one driver, now. The rider's patience is ~1–3 minutes before they cancel; the driver's attention per offer is ~10–15 s. The system optimizes pickup ETA and match rate under tight latency, with strict single assignment. Pricing adjusts in near-real time to pull supply toward demand.
Intent 2 — On-demand delivery. Three parties: eater, merchant, courier. The critical constraint is food readiness: dispatching a courier the moment an order is placed wastes courier time at the restaurant (idle courier cost) and dispatching too late delivers cold food. Couriers can carry multiple orders (batching), so assignment is a routing problem with pickup/dropoff precedence. ETA accuracy is the product.
Intent 3 — Scheduled logistics. Grocery windows, freight, school buses. Hours of planning horizon, cost-per-stop objective, vehicle routing optimization solved in batch. Real-time tracking matters; real-time matching doesn't.
| Dimension | Ride Hailing | Delivery | Scheduled Logistics |
|---|---|---|---|
| Dispatch timing | Immediately | Prep-time aware (often delayed 5–15 min) | Hours ahead |
| Assignment unit | 1 rider ↔ 1 driver | N orders ↔ 1 courier | Many stops ↔ 1 vehicle route |
| Primary objective | Pickup ETA, match rate | Delivered-hot ETA, courier utilization | Cost per stop |
| Solver | Greedy or small batch (seconds) | Batch with prep predictions (seconds–minutes) | VRP optimizer (minutes–hours) |
| Key failure | Double assignment, stranded rider | Cold food, courier waiting | Infeasible plan |
2.2 When NOT to Use This Architecture#
| Situation | Why It's Wrong | Use Instead |
|---|---|---|
| Supply chooses demand (freelancers browse jobs) | No assignment problem; it's search + contention on accept | Marketplace listing + Flash Sales-style accept |
| < ~10K concurrent drivers in total | A single Postgres + PostGIS handles location and trips in one place with row locks | Monolith, one database, add planes later |
| Scheduled-only operations | Real-time dispatch adds cost and complexity with no benefit | Batch VRP with a manifest service |
| Mutual choice between peers | Two-sided consent, not assignment | Proximity Matching |
| Fixed routes (buses, shuttles) | No matching at all; just tracking | Location ingestion + Real-Time Updates |
🎯 Staff Move: "Under ~10K concurrent drivers I'd run this as one service on one Postgres with PostGIS, and pings every 5–10 s. The three-plane architecture is what you grow into when the ping rate stops fitting in a database — I'd name that trigger rather than build for it on day one."
2.3 What the Interviewer Leaves Underspecified#
| Underspecified | Why It Matters | What to Say |
|---|---|---|
| Scale | Whether you need planes at all | "~1M online drivers at peak, ~25M trips/day." |
| Ping interval | Ingestion cost and freshness | "4 s while online, 1–2 s during active trip for tracking smoothness, 30 s when idle-parked." |
| Products (pool, XL, delivery) | Dispatch complexity | "UberX first; pool and delivery as extensions." |
| Pricing model | Upfront vs metered | "Upfront quote locked for a few minutes; final fare may adjust for major route changes." |
| Payment timing | Where risk sits | "Pre-auth at request, capture at completion." |
| Regions | Residency, failover | "Each city has a home region." |
| Cancellation policy | State machine branches | "Free cancel within ~2 min of assignment; fee after." |
2.4 Precise Terminology#
| Term | Meaning Here | Common Confusion |
|---|---|---|
| Dispatch | Choosing which driver gets an offer for which request | Not the same as "matching" in dating; the platform decides |
| Offer | A time-limited proposal to one driver | Not an assignment until accepted and committed |
| Lease | A TTL'd exclusive claim on a driver for one offer | Prevents double-offer; expires automatically |
| Assignment | Committed trip transition to DRIVER_ASSIGNED | The durable fact; the lease is advisory |
| ETA | Predicted travel time on the road network, with traffic | Not straight-line distance |
| Deadhead | Driver miles without a passenger | The cost dispatch minimizes alongside rider wait |
| Surge / dynamic pricing | Multiplier on base fare per area and time | Not the same as the upfront quote, which includes it |
| Quote lock | The window during which the shown price is honored | Prevents bait-and-switch between quote and request |
| Pre-authorization | A hold on the rider's card for the estimated amount | Not a charge; expires after days if not captured |
| Capture | Converting a hold into a charge for the final amount | Must be idempotent |
| Outbox | Events written in the same transaction as the state change, relayed later | Prevents "state changed but event lost" |
| Map-matching | Snapping noisy GPS points to road segments | Required before computing distance-based fares |
3. The Fault Lines#
3.1 Fault Line 1: Location Freshness vs Ingestion Cost#
| Strategy | What Works | What Breaks | Who Pays |
|---|---|---|---|
| Every ping to a durable DB | Full history; simple queries | ~250K writes/s of mostly superseded data; contends with trip writes; $$$ | Infra budget; trip latency |
| Latest in Redis GEO, history to Kafka | Fast GEOSEARCH; history decoupled | Redis cluster per city must absorb bursts on reconnect; single-threaded shards hot in big cities | Platform team runs many Redis clusters |
| Latest in the dispatch process's own memory, sharded by city/cell (Staff default at scale) | Zero network hop for dispatch reads; state co-located with the consumer | Process restart loses state → rebuilt from pings in one interval (~4 s); needs sticky routing of pings to owner | Dispatch team owns routing and rebuild |
| Adaptive ping rate | Online-idle 15–30 s, available 4 s, on-trip 1–2 s | Client complexity; server must tolerate varied freshness | Mobile team |
The Staff default: latest location in memory, owned by the city/cell shard that dispatches for that area; pings routed by h3_cell → shard through the connection gateway; every ping also appended to Kafka for history, ETA-model training, fraud and fare disputes. Losing a shard's memory costs one ping interval — the ping stream is the replication mechanism.
Adaptive intervals cut ingestion roughly in half: a typical fleet spends ~40–50% of online time idle or parked, where 15–30 s pings are enough.
When to deviate: under ~50K online drivers, Redis GEO per region is simpler and plenty fast (~0.1–1 ms per GEOSEARCH). Move state into the dispatcher only when network hops and Redis hot shards show up in dispatch p99.
🎯 Staff Move: "I don't replicate driver location. The driver's phone replicates it for me every 4 seconds. If a shard dies, its index is rebuilt by the next round of pings — so durability here would be paying for something the protocol already gives us."
3.2 Fault Line 2: Greedy vs Batched Dispatch#
Greedy total pickup ETA: 11 min. Batched: 5 min — at the cost of up to 3 s added to Rider 1's wait.
| Strategy | What Works | What Breaks | Who Pays |
|---|---|---|---|
| Greedy nearest-ETA | Lowest time-to-offer; trivially parallel per request | Globally wasteful in dense areas; long tail of bad pickups | Riders with 9-minute pickups; drivers' deadhead |
| Batched assignment (2–5 s) (Staff default in dense zones) | Industry-reported ~5–15% lower total ETA in dense markets | Latency floor of the window; one solver per zone is a hot spot and a SPOF | Every rider pays the window; dispatch team owns the solver |
| Adaptive | Batch where density is high, greedy where sparse | Two code paths; boundary behavior | Dispatch team |
The Staff default: adaptive. Per H3 res-7 zone, if pending requests × available drivers exceeds a threshold (e.g., ≥ 3 × 3), run a batch every 2–3 s using min-cost assignment on an ETA matrix; otherwise greedy. The ETA matrix is the expensive part: 50 requests × 200 candidates = 10K ETA lookups per batch — served from a precomputed road-graph routing engine with per-segment traffic, ~sub-ms per lookup.
When to deviate: delivery changes the window entirely — the "batch" is minutes and includes when to dispatch. Airport queues are FIFO by regulation or policy, not ETA — a separate dispatcher.
🎯 Staff Move: "Batching is a policy trade: every rider pays ~2–3 s so that the tail rider doesn't pay 7 minutes. I'd only turn it on where density makes the tail real, and I'd measure p90 pickup ETA, not average."
3.3 Fault Line 3: Assignment Consistency — Lease vs Transaction vs Detect-and-Repair#
The invariant: a driver is attached to at most one active trip, and a trip to at most one driver.
| Strategy | What Works | What Breaks | Who Pays |
|---|---|---|---|
| Global transaction on drivers + trips tables | Obviously correct | Cross-shard 2PC or a single hot database; tens of ms under contention; spike collapse | Everyone at peak |
| Detect and repair (offer freely, fix conflicts after accept) | Fast offers | Driver accepts two trips; one rider stranded; apology credits | Riders, support, brand |
| Offer lease per driver + conditional trip transition (Staff default) | Lease prevents double-offer cheaply (SET NX PX); trip transition is the durable commit with a version check | Lease store is a dependency; lease and trip can disagree briefly | Dispatch team owns lease store; trip team owns the commit |
| Single-writer dispatcher per zone | No locks; the zone's dispatcher is the only one that can offer its drivers | Zone boundaries: drivers near an edge; failover of the zone owner | Dispatch team owns zone ownership and handoff |
The lease is advisory; the trip transition is authoritative:
# Dispatch: claim a driver for an offer
ok = lease_store.set("offer:" + driver_id, trip_id, NX, PX=15000)
if not ok: try next candidate
# Accept: authoritative commit (trip store, per-trip linearizable)
BEGIN
trip = SELECT ... FROM trips WHERE trip_id = :t FOR UPDATE
drv = SELECT ... FROM driver_state WHERE driver_id = :d FOR UPDATE
if trip.state != 'MATCHING' or drv.status != 'available': ROLLBACK → "offer expired"
UPDATE trips SET state='DRIVER_ASSIGNED', driver_id=:d, version=version+1
UPDATE driver_state SET status='en_route', current_trip_id=:t
INSERT INTO outbox (trip_id, event='driver_assigned', version=...)
COMMIT
Trip and driver state live in the same city-sharded store so this is a single-shard transaction — the reason city sharding matters for correctness, not just scale.
When to deviate: a driver crossing a city boundary mid-shift needs a handoff between shards: the old shard marks the driver migrating, the new shard takes ownership on the next ping, and offers are paused for that driver for one ping interval.
🎯 Staff Move: "The lease stops the obvious race cheaply. But the lease can expire while an accept is in flight, so the accept is a conditional transaction on the trip and driver rows in the same shard. The lease is a hint; the commit is the truth."
3.4 Fault Line 4: Surge — Market Efficiency vs Fairness and Stability#
| Strategy | What Works | What Breaks | Who Pays |
|---|---|---|---|
| No surge (fixed prices) | Simple; no headlines | Demand spikes → no drivers; riders wait 20+ min or get nothing | Riders (unavailability), drivers (no incentive) |
| Raw ratio per hex, every minute | Responsive | Oscillation: price up → demand drops, drivers arrive → price crashes → repeat; neighbor hexes differ wildly (walk 100 m, pay 2×) | Riders (unpredictability), trust |
| Smoothed control loop with caps and quote lock (Staff default) | Stable; spatially smooth; honors shown price | Slower to react (1–2 min); caps leave some demand unserved | Business (unserved demand at cap) |
| Driver incentives instead of rider price | Softer rider experience | Slower supply response; costlier | Finance |
Surge as a control loop:
every 60–120 s, per H3 res-7 hex h:
demand = requests + app_opens_with_quote in last 5 min (h)
supply = available drivers + drivers finishing trips within 5 min (h)
raw = f(demand / supply) # tuned curve, not a ratio
spatial = weighted avg of raw over k-ring(h, 1) # no 2× cliffs between neighbors
temporal = α · spatial + (1 − α) · previous(h) # α ≈ 0.3–0.5, damps oscillation
mult(h) = clamp(temporal, 1.0, cap(h, time)) # cap from policy; emergency caps
publish versioned snapshot; quotes embed snapshot version
The Staff point is ownership: the curve f, the cap policy and the emergency override are owned by the Marketplace/Pricing team with Legal sign-off. Uber has publicly committed to capping surge during declared emergencies in some jurisdictions — the kind of external commitment engineering must be able to enforce with a kill switch that works in seconds.
🎯 Staff Move: "Surge is a feedback controller, so I treat it like one: smoothing, damping, bounded output, and a manual override. And the cap table isn't config engineers edit — it's policy with a legal owner."
3.5 Fault Line 5: Trip State Durability vs Availability — and the Payment Handoff#
| Strategy | What Works | What Breaks | Who Pays |
|---|---|---|---|
| Status column, any service writes | Easy | Illegal transitions (COMPLETED → DRIVER_ASSIGNED); lost updates; no audit | Support, finance reconciliation |
| State machine in a strongly consistent, city-sharded store with outbox (Staff default) | Linearizable per trip; transitions validated; events never lost | Region failover must move the city's store; costlier writes | Trip platform team |
| Event-sourced trip log | Full history, replayable | Read models lag; complexity for every consumer | Every consuming team |
| Workflow engine (Cadence/Temporal-style) owns the lifecycle | Timers (no-show, matching timeout) and retries are first-class | Another critical dependency; workflow versioning | Platform team |
Payment handoff — the saga:
Rules that make it safe:
- Every processor call carries an idempotency key derived from
trip_id + step— retries never double-charge. See Payment Processing. - Pre-auth failure at request time: for new or risky riders, block the request (fail-closed); for trusted riders, proceed and try again (fail-open) — a risk-policy decision owned by Payments/Risk.
- Capture failure: retry with backoff over hours; try a backup payment method; move trip to
ARREARSand block the rider's next request until settled. - Driver earnings are credited at
COMPLETED, notSETTLED. The company absorbs collection risk; the driver is never the victim of a rider's card.
🎯 Staff Move: "The trip and the payment are two state machines linked by events. The trip can finish while the payment is still trying. The only coupling is policy: whether a risky rider can start a trip without a successful pre-auth — and that's a Risk team decision, not a timeout I pick."
4. Failure Modes & Operational Reality#
4.1 Double Assignment Under a Spike#
t=0 Stadium concert ends. 3,000 requests in 4 minutes in 6 hexes.
t=+20s Lease store (one Redis shard for the city) at 100% CPU; SET NX p99 → 400 ms.
t=+40s A code path added last month falls back to "offer without lease" on lease
timeout to "keep matching moving."
t=+60s 148 drivers receive two offers; 61 accept both within the window.
t=+3min 61 riders see a driver who is going somewhere else. Cancellations spike.
- Detection:
dispatch.double_offer_total(should be 0),trip.assign_conflict_total(conditional commits rejected),lease.latency_p99. - Blast radius: one city, event-driven spikes.
- Mitigation: remove the fallback — on lease timeout, skip the candidate, never offer unleased; shard the lease store by H3 res-6; admission control on requests per hex (queue with an honest ETA rather than overload).
- Prevention: invariant test in CI ("no offer without lease"); lease store sized for 10× city peak.
- Owner: Dispatch team.
4.2 Driver Reconnection Storm#
t=0 A cellular carrier has a 3-minute outage across one metro.
t=+3min Carrier recovers. 40K drivers reconnect within ~10 s.
t=+3min Connection gateway TLS handshakes spike 50×; ping backlog replays.
t=+4min Location index for the metro is 3 minutes stale; dispatch offers drivers
who have since moved 2 km. Pickup ETAs are wrong by 5+ minutes.
- Detection:
conn.handshakes_per_s,location.staleness_p99_s{city}> 15 s,dispatch.eta_error_p90. - Mitigation: clients reconnect with jittered backoff (0–30 s); clients drop queued pings older than 10 s and send only the latest; dispatch excludes drivers with location older than 20 s.
- Prevention: TLS session resumption; per-city connection gateway capacity for 3× reconnect; staleness filter always on.
- Owner: Edge/connection platform (reconnect), Dispatch (staleness filter).
4.3 Surge Oscillation#
- Symptom: a downtown hex flips between 1.0× and 2.8× every 2 minutes; social media screenshots; drivers chase surges that vanish on arrival.
- Root cause: α = 1.0 (no temporal smoothing) after a config change; no neighbor smoothing on a new city launch.
- Detection:
surge.multiplier_volatility{hex}(std dev over 15 min),surge.neighbor_delta_max. - Mitigation: restore smoothing; freeze surge snapshot for the city while investigating.
- Owner: Pricing team; config change process owned with Marketplace.
4.4 Payment Processor Degradation#
t=0 Card processor p99 goes 300 ms → 8 s in one region.
t=+1min Pre-auth calls time out; if the request path awaits pre-auth, requests stall.
t=+5min Capture retries pile up; outbox relay lag grows.
- Detection:
payment.auth_latency_p99,payment.capture_retry_queue_depth,outbox.relay_lag_s. - Blast radius: with the saga design — none to trips; collection is delayed. With a synchronous design — every new request in the region.
- Mitigation: trust-tier policy: trusted riders proceed without completed pre-auth; circuit-break the processor and route to a secondary processor where contracted; capture retries with exponential backoff over hours.
- Owner: Payments team; risk policy owned by Risk.
4.5 Trip Store Region Failure Mid-Trip#
- Scenario: the home region for 30 cities loses its trip store primary. ~200K trips are in progress.
- What must not happen: trips vanish; drivers can't complete; riders are double-charged after recovery.
- Mitigation: trip store replicated synchronously across zones within the region (survives a zone); for a region loss, promote the async replica in the paired region (RPO of seconds). Clients cache the current trip state and version locally and re-submit their last action with the same idempotency key after failover. Transitions lost in the RPO window are reconstructed from client-held state and location history (e.g., "completed" inferred from driver app + GPS at dropoff).
- Detection:
trip.write_errors{region},trip.client_resubmits_total. - Owner: Trip platform + SRE.
4.6 GPS Drift and Fare Disputes#
- Symptom: urban canyons produce 50–100 m jumps; naive distance summation inflates fares by 5–15% on some routes.
- Mitigation: map-matching to road segments before distance calculation; upfront pricing so the fare is mostly fixed at request; disputes resolved from matched route.
- Owner: Maps/Routing team; Fares team.
4.7 Operational Reality Matrix#
| Failure | Detection Signal | Blast Radius | Mitigation | Owner |
|---|---|---|---|---|
| Double assignment | dispatch.double_offer_total > 0 | One city at spikes | Skip unleased candidates; admission control | Dispatch |
| Reconnection storm | location.staleness_p99_s > 15 | One metro | Jittered reconnect; staleness filter | Edge platform |
| Location shard crash | location.index_size drop | One zone for ~4 s | Rebuild from next pings | Dispatch |
| Surge oscillation | surge.multiplier_volatility | Hexes in one city | Smoothing, freeze snapshot | Pricing |
| Processor degradation | payment.auth_latency_p99 | Collection delay only | Trust tiers, secondary processor | Payments |
| Trip store failover | trip.write_errors | Cities homed in region | Replica promotion, client resubmit | Trip platform |
| ETA service down | eta.errors | Dispatch quality | Fallback to haversine × city speed factor (fail-open) | Routing |
| Outbox relay stalled | outbox.relay_lag_s > 60 | Payments, receipts, surge inputs | Restart relay; events are durable in outbox | Trip platform |
| GPS drift | fare.dispute_rate | Individual fares | Map-matching, upfront pricing | Maps / Fares |
🎯 Staff Move: "ETA fails open to a crude estimate, location fails to one-ping staleness, payments fail to arrears — but trip transitions never fail open. That's the one place we'd rather reject a request than guess."
5. Evaluation Rubric#
5.1 Level-Based Signals#
| Dimension | Senior (L5) | Staff (L6) | Principal (L7) |
|---|---|---|---|
| Scoping | Rider/driver features | Three intents; commits to rides; explains how delivery changes dispatch timing | Identifies shared platforms across rides/delivery/freight and product-owned policies |
| Sizing | Trips per day | 250:1 location-to-trip ratio drives plane separation | Converts to $/month per plane; knows which plane dominates cost |
| Location | GPS table + geo index | In-memory latest per city, async history, pings as replication | Org retention/privacy policy for location history; data as a shared asset |
| Dispatch | Nearest driver | ETA ranking, leases, adaptive batching, conditional commit | Dispatch objective governance, driver fairness, experimentation platform |
| State | Status column | Explicit state machine, single owner, outbox | Trip lifecycle as a versioned org contract consumed by 20+ teams |
| Payments | Charge at end | Pre-auth/capture saga, idempotency, trip never blocks | Risk envelope, fraud-loss budget, payout guarantees by market |
| Pricing | demand/supply | Smoothed control loop, caps, quote lock | Surge as regulated pricing: legal commitments, audit trail, emergency caps |
| Resilience | Replicas | City-as-failure-unit, region pairing, client resubmit | Cell architecture, error budgets per plane, regional game days |
5.2 Strong Hire Signals#
| Signal | What It Sounds Like |
|---|---|
| Classifies data by loss tolerance | "If we lose a ping, nothing happens. If we lose 'completed', a driver can't take the next trip." |
| Structural contention fix | "A driver holds one lease. The accept is a conditional commit on the same shard." |
| Explicit state machine | "No service writes status. They send commands; the trip service validates transitions." |
| Payment decoupled with policy | "The trip completes regardless. Whether we require pre-auth is a risk policy." |
| Control-loop surge | "Smoothing, damping, caps, override — it's a controller." |
| Names owners | "Pricing owns the curve; Legal owns the caps; Dispatch owns leases." |
5.3 Lean No-Hire Signals#
| Signal | Why It Misses the Bar |
|---|---|
| GPS pings written to the trips database | No data classification; will not survive scale |
| "Lock the driver row" with a global database | Correct but ignores the throughput and blast radius |
| Synchronous payment in the request path | Couples rider experience to a third party's latency |
| Surge as a pure ratio | Ignores feedback dynamics and fairness |
| No cancellation or no-show states | State machine incomplete; real trips hit these constantly |
| Straight-line distance for candidate ranking | Misranks drivers across rivers, highways, one-way grids |
5.4 Common False Positives#
- Geo-index fluency ≠ dispatch design. Knowing H3 resolutions is table stakes.
- "We'll use Kafka" ≠ event-driven correctness. Without an outbox, state and events diverge.
- Optimization vocabulary (Hungarian algorithm) ≠ marketplace judgment. The signal is knowing when batching's latency is worth it.
- Multi-region buzzwords ≠ trip durability. "Active-active" without saying who owns a trip mid-failover is a red flag.
6. Interview Flow & Pivots#
6.1 Typical 45-Minute Shape#
| Phase | Time | Goal |
|---|---|---|
| Framing, intents, ratios | 0–4 min | 250:1, loss tolerance classes, commit to rides |
| Entities and API | 4–6 min | Versioned trip commands, idempotency, driver socket |
| Architecture | 6–11 min | Three planes, city sharding |
| Dispatch and double assignment | 11–19 min | ETA, lease, batch, conditional commit |
| Trip state machine and payments | 19–28 min | States, outbox, saga, risk policy |
| Location and surge | 28–37 min | In-memory, reconnect storms, control loop |
| Multi-region / delivery extension | 37–42 min | City home region; prep-time dispatch |
| Wrap-up | 42–45 min | Metrics, evolution, skipped scope |
6.2 How Interviewers Pivot — And What They're Testing#
| Pivot | What They're Testing | Strong Response |
|---|---|---|
| "Now add UberPool / shared rides" | Assignment as routing; state machine with multiple riders | "A vehicle trip holds N rider legs; insertion cost into the current route is the dispatch score; each leg has its own state machine." |
| "Now it's food delivery" | Intent shift | "Dispatch time = predicted ready time − courier ETA; order batching; merchant is a third state machine." |
| "Show the driver's car moving smoothly on the rider's map" | Real-time push | "Push via the rider's socket every 1–2 s during approach, interpolate client-side." See Real-Time Updates. |
| "A rider says the driver took a longer route" | Location history, fare integrity | "Map-matched route from history; upfront pricing limits exposure." |
| "Launch in a new country" | Residency, payments rails, regulation | "New home region if required; local payment methods (cash!) change the saga." |
| "The ETA service is down" | Degraded mode | "Haversine × per-city speed factor; flag ETAs as approximate; batching off." |
6.3 What to Deliberately Skip#
| Topic | Why Skip | One-Liner If Asked |
|---|---|---|
| Routing/ETA algorithm internals | Separate system | "Contraction hierarchies over a road graph with live traffic; I'll treat ETA as a service." |
| Map tile rendering | Not relevant | "Client SDK, CDN." |
| Driver onboarding, background checks | Business workflow | "Separate workflow system." |
| Ratings | Simple aggregate | "Async rating events, rolling average." |
| Fraud models | Separate team | "Risk scores feed the pre-auth policy." |
6.4 Follow-Up Questions to Expect#
- "Two riders request within 100 ms and the best driver is the same. What happens?"
- "A driver accepts, then their phone dies. What does the rider see, and when?"
- "How many writes per second hit your system, and which are durable?"
- "The card capture fails. Is the driver paid?"
- "Surge in one hex is 3× and the next hex is 1×. Is that a problem?"
- "A region dies. What happens to trips in progress?"
- "How would you change dispatch for food delivery?"
7. Active Drills#
Drill 1: The Opening#
Prompt: "Design Uber."
Staff Answer
"Three intents hide here — ride hailing, delivery, scheduled logistics. I'll design ride hailing and show what changes for delivery.
Two flows with very different shapes: ~1M online drivers pinging every 4 s is ~250K location writes/s; trips are ~25M/day, ~1K requests/s at peak. Location is superseded every 4 s, so losing it is free. Trip transitions move people and money, so losing them is never acceptable.
That gives me three planes: an in-memory location plane sharded by city, a per-city dispatch plane that holds one offer lease per driver, and a durable trip plane — a state machine with an outbox — that feeds payments, surge and receipts asynchronously. Payment is a saga that never blocks the ride."
Why this is L6:
- Classifies data by loss tolerance before choosing storage
- Commits to an intent and previews the delivery delta
- Payment decoupled from the ride from the first minute
What L7 adds:
- Names location, ETA and payments as shared platforms across rides and delivery, with product-owned dispatch policy
- Notes that the ETA model is the most expensive shared asset and divergent copies would contradict each other to customers
Drill 2: Core Mechanic — Driver Accepts, Phone Dies#
Prompt: "The driver taps Accept and their phone dies before the response arrives. What happens?"
Staff Answer
"Two cases. If the accept never reached us, the lease expires at 15 s and the trip goes back to the next candidate — the rider sees 'finding your driver' a few seconds longer. If the accept reached us, the trip is DRIVER_ASSIGNED durably and the rider sees the driver. Now the driver is silent: no pings. Liveness rule: if an assigned driver's location is older than ~60 s and they haven't moved toward pickup, we notify the rider, and at ~2 min we move the trip back to MATCHING with a driver_unresponsive reason and release the driver to offline. When the phone comes back, the driver app fetches its current state by version and sees the trip was reassigned. The rider isn't charged a cancellation fee; the driver's acceptance-rate metric isn't penalized for a device failure if we can detect it."
Why this is L6:
- Distinguishes 'accept lost' vs 'accept committed'
- Defines liveness timeouts with numbers and a reassignment transition
- Considers fairness to both parties
What L7 adds:
- Makes driver-penalty rules a policy with a driver-advocacy owner, not a hard-coded metric
- Tracks unresponsive-driver reassignment rate per device model and OS version as a platform-quality signal
Drill 3: Make It Concrete — Location Ingestion Sizing#
Prompt: "Size location ingestion and tell me where every byte goes."
Staff Answer
"1M online drivers at peak; adaptive intervals: 40% idle at 15 s, 45% available at 4 s, 15% on trip at 2 s. That's 1M × (0.4/15 + 0.45/4 + 0.15/2) ≈ 1M × (0.027 + 0.1125 + 0.075) ≈ 215K pings/s. At ~150 bytes each, ~32 MB/s, ~2.8 TB/day.
Where it goes: (1) connection gateway → the city's dispatch shard in-memory index (latest only, ~200 bytes × 1M = 200 MB total — trivial); (2) Kafka topic partitioned by city, 3-day retention for replay (~8.4 TB); (3) a consumer compacts to 30-second resolution for ETA training and disputes into object storage (~10× smaller, ~280 GB/day); (4) on-trip pings at full resolution attached to the trip record for fare integrity, 2-year retention.
I'd set retention with Privacy: full resolution only for trips; idle-driver history downsampled and deleted after ~30 days."
Why this is L6:
- Numbers with adaptive rates
- Every sink has a purpose and a retention
- Privacy as a design input
What L7 adds:
- Prices storage and Kafka ($/TB) and decides retention by business value per byte
- Sets a company-wide location data policy across products
Drill 4: Dependency Down — ETA Service Outage#
Prompt: "The ETA service is returning errors for one city."
Staff Answer
"Dispatch falls back to haversine distance × a per-city, per-hour speed factor (e.g., 18 km/h downtown at rush hour) — precomputed and cached in the dispatcher. Batching is disabled in that city because the optimizer on bad ETAs does worse than greedy. Rider-facing ETAs are shown as ranges. Upfront pricing uses the last good route estimate for the O-D pair or falls back to a distance-based fare with a conservative cap.
Alert eta.fallback_ratio{city} > 10% for 5 min → page Routing. The trip plane is untouched."
Why this is L6:
- Concrete fallback with a precomputed factor
- Knows to disable batching when inputs are degraded
- Protects pricing from bad estimates
What L7 adds:
- Error budget for ETA shared by rides and delivery; delivery is more sensitive and may need priority
Drill 5: Hot Key — Airport Queue and Stadium Exit#
Prompt: "500 drivers wait at an airport lot; 3,000 riders request at once after a concert. What's hot?"
Staff Answer
"Airport: dispatch is FIFO by queue position (airport policy), not ETA. The queue is one hot key per airport — a sorted set of driver IDs by entry time on a single shard, ~1–5 ops/s per driver arrival and dispatch, fine. The hot part is geofence verification: drivers must be inside the lot to hold position.
Stadium: 3,000 requests in a few hexes means the dispatch shard for that zone and the lease store are hot. Mitigations: pre-planned event mode (schedule known from ticketing data) with extra dispatcher capacity for that zone, pickup zones that spread riders across 4–6 locations, request admission with an honest 'high demand — ETA 12 min' rather than overload, and surge to pull supply before the doors open."
Why this is L6:
- Different hot keys, different fixes
- Uses product (pickup zones, honest ETA) as load control
- Pre-planned capacity for scheduled events
What L7 adds:
- Partnerships with venues and airports as part of capacity planning; event calendars feed capacity automatically
Drill 6: Multi-Tenant — Rides and Delivery on One Platform#
Prompt: "Should rides and delivery share dispatch?"
Staff Answer
"Share the primitives, not the policy. Shared: location plane, ETA, lease store, the fulfillment state machine framework, payments. Separate: the dispatch objective (rides: pickup ETA now; delivery: ready-time-aware, batching), the solver cadence, and the state machine definitions.
The interesting shared case is a driver who does both. Then supply is shared and the platforms compete for the same driver — the lease store must be global per driver across products, so a driver can't hold a ride offer and a delivery offer at once. Priority between products for a shared driver is a marketplace policy decision, owned above both product teams."
Why this is L6:
- Platform/policy split
- Catches the shared-supply lease problem
- Escalates the priority decision to the right owner
What L7 adds:
- Designs the org: a fulfillment platform team providing primitives, product dispatch teams owning objectives, and a marketplace council arbitrating shared supply
Drill 7: Build vs Buy — Trip State Store#
Prompt: "Build your own trip store on sharded MySQL, or buy something like Spanner?"
Staff Answer
"The trip store needs per-trip linearizability, multi-row transactions (trip + driver state), cross-zone synchronous replication and fast failover. Sharded MySQL/Postgres by city gives the first two if trip and driver share a shard; replication and failover are ours to build and operate. A managed globally consistent store gives all four at a higher unit price.
Decision rule: if we have fewer than ~3 engineers who can own sharding, failover and resharding for years, buy. Uber publicly moved its fulfillment platform onto Spanner, which is a strong signal of where the cost curve lands at very large scale. I'd start on sharded Postgres by city with an abstraction over the store and revisit when cross-city transactions (e.g., shared drivers across cities) become common."
Why this is L6:
- Requirements first, then options
- Staffing as a decision input
- Keeps an abstraction to preserve the option
What L7 adds:
- Vendor lock-in and exit cost analysis; negotiates committed-use pricing; plans the migration path if needed
Drill 8: Policy Change Without Outage — New Surge Curve#
Prompt: "Pricing wants a new surge curve launched Friday."
Staff Answer
"Shadow first: compute both curves for 2 weeks and compare multipliers, volatility and projected conversion. Then city-level A/B (switchback experiments by time slot, since riders in one city interact through shared supply — user-level A/B is contaminated). Guardrails: completion rate, rider wait p90, surge.multiplier_volatility, driver earnings per hour. Caps and emergency override unchanged. Rollback is a versioned config snapshot, effective at the next 60 s cycle. Legal reviews the curve if it changes maximum multipliers."
Why this is L6:
- Switchback experiments because of marketplace interference
- Guardrails on both sides of the market
- Config rollback within one cycle
What L7 adds:
- Establishes a pricing-change review with Legal and Policy and a public-facing explanation where regulators require it
Drill 9: Cost#
Prompt: "Infra costs per trip rose 40%. Where do you look?"
Staff Answer
"Per-trip cost is mostly the location plane and ETA. Check: (1) ping interval regressions — a client release that dropped adaptive intervals doubles ingestion; (2) ETA calls per dispatch — batching with 200 candidates per request is 10K lookups per batch, so a candidate-radius change multiplies cost; (3) Kafka retention on location history grew; (4) surge recompute resolution changed from res 7 to res 8 (7× more hexes). I'd publish cost per trip by plane weekly with owner attribution."
Why this is L6:
- Knows the cost drivers and their multipliers
- Attribution by plane
What L7 adds:
- Unit economics tied to take rate; decides which cost reductions are worth quality loss with Finance
Drill 10: Multi-Region#
Prompt: "Make it survive a region outage."
Staff Answer
"Cities are the unit. Each city has a home region; the trip store for that city replicates synchronously across 3 zones in-region and asynchronously to a paired region (RPO ~1–5 s). Location and dispatch state are rebuildable — after failover, the connection gateway in the paired region receives pings and rebuilds the index within one interval.
Failover: promote the replica, flip the city's home in the routing table, clients reconnect. In-flight trips: clients hold state and version; they re-submit their last action idempotently. Transitions lost in the RPO window are reconciled from client state and location history. I would not run active-active writes for the same trip in two regions — a trip needs one owner. Game day: fail a mid-size city quarterly."
Why this is L6:
- City as failure unit; single-owner trips
- Rebuildable vs durable state distinguished
- Client-assisted recovery with idempotency
What L7 adds:
- Cell-based architecture across the org with blast-radius targets (no single failure affects > X% of trips)
- Budgets the paired-region capacity cost explicitly
8. Deep Dive Scenarios#
Deep Dive 1: New Year's Eve — Assignment Latency Collapse#
Context: 00:02 local, one of your top-5 cities. Request-to-assignment p50 jumped from 12 s to 90 s; cancellations are 4× normal. The on-call escalates to you.
Questions to Surface First:
- Is supply exhausted (no available drivers) or is dispatch slow (drivers available but not offered)?
- What are lease store and dispatch shard latencies in the hot zones?
- Is the batch solver timing out?
- Is surge responding, and are drivers moving toward hot hexes?
Typical L5 Approach: Scales the dispatch service horizontally and increases the lease store's memory. Doesn't check whether the solver or the supply is the bottleneck.
Staff Approach: Splits the problem in 2 minutes:
drivers_available{zone}vsdispatch.pending_requests{zone}. Finds drivers available but solver batches taking 8 s (candidate set exploded because requests widened search radius). Caps candidates per request to 30 by ETA, shrinks batch window to 1 s in the hot zones, and turns on pickup-zone steering. Surge is at cap; confirms caps are policy, not a bug.
Principal Approach: Asks why the city's top event of the year wasn't in an event calendar with pre-provisioned solver capacity and pre-tuned parameters. Makes "known peak events" a planning artifact owned jointly by Marketplace Ops and Dispatch, reviewed 2 weeks ahead, with a load test at 3× last year's peak.
Staff Approach — Full Reasoning
| Phase | What to Do |
|---|---|
| Immediate (0–5 min) | Supply vs dispatch split; cap candidate set; shorten batch window |
| Triage | Solver time per batch, ETA lookups per batch, lease latency |
| Quick fix | Greedy fallback in zones where solver p99 > 2 s |
| Guardrails | Watch double-offer count = 0 and cancellation rate |
| Post-mortem | Solver time budget enforced automatically; event calendar |
Metrics to Watch: dispatch.assign_latency_p50{city}, dispatch.solver_time_p99{zone}, dispatch.candidates_per_request, drivers_available{zone}, trip.cancel_rate
Organizational Follow-up: Solver time budget with automatic greedy fallback; event calendar owned by Marketplace Ops.
Ownership Question: "Who decides to switch a city from batched to greedy during an incident?" Staff answer: The dispatch on-call, pre-authorized by runbook when solver p99 > 2 s for 2 minutes. It's automatic next time.
Key Takeaway: "When a solver's inputs grow, its runtime grows faster. Bound the inputs before you scale the fleet."
What clears the Staff bar:
- Separates supply shortage from dispatch slowness
- Bounds solver inputs
- Turns manual switch into an automatic guardrail
Deep Dive 2: The Silent Failure — Drivers Unpaid for Completed Trips#
Context: Driver support tickets show ~0.3% of completed trips missing from weekly earnings. No alerts fired.
Questions to Surface First:
- Are the trips
COMPLETEDin the trip store? - Did the
trip_completedevents reach the earnings consumer? - Is there an outbox relay gap or a consumer skipping on error?
- Is it correlated with a region, app version, or payment status?
Typical L5 Approach: Finds a consumer bug that dropped events on deserialization errors and backfills the missing earnings.
Staff Approach: Finds the root: the earnings consumer caught schema errors after a trip event schema change (new optional field with a non-default type), logged them, and committed offsets — silently dropping events. Backfills from the outbox table (the source of truth). Adds a DLQ with an alert on depth > 0, and a daily reconciliation:
COMPLETED tripsvsearnings linesper driver per day; mismatch > 0 pages.
Principal Approach: Treats the trip event as a versioned public contract. Establishes a schema registry with compatibility enforcement in CI for all 20+ consumers, and a rule that money-moving consumers never skip — they stop and page. Makes the earnings reconciliation report visible to Driver Operations leadership.
Staff Approach — Full Reasoning
| Phase | What to Do |
|---|---|
| Immediate | Confirm scope via outbox vs earnings diff |
| Triage | Identify the schema change and the skip-on-error path |
| Quick fix | Backfill from outbox; notify affected drivers with amounts |
| Guardrails | DLQ + alert; money consumers halt on error |
| Post-mortem | Schema compatibility checks; daily reconciliation |
Metrics to Watch: earnings.reconcile_mismatch_total, consumer.dlq_depth{consumer}, consumer.deserialize_errors_total
Organizational Follow-up: Schema registry owned by Trip platform; consumers register and are notified of changes.
Ownership Question: "Who owns making the drivers whole?" Staff answer: Driver Earnings team, same week, with an explanation — and the fix is funded before the post-mortem closes.
Key Takeaway: "A consumer that moves money must never skip an event. It stops, and someone gets paged."
What clears the Staff bar:
- Uses the outbox as a source of truth for recovery
- Adds reconciliation, not just a bug fix
- Classifies consumers by criticality
Deep Dive 3: Large-Customer Onboarding — Enterprise Delivery Partner#
Context: A national grocery chain wants to route 200K orders/day through your delivery platform, with scheduled windows and bulk orders (courier capacity limits matter).
Questions to Surface First:
- What fraction is on-demand vs scheduled windows?
- Vehicle capacity constraints (a 40-item order doesn't fit on a bike)?
- SLA commitments and penalties?
- Integration: API volume, order bursts at window open?
Typical L5 Approach: Adds a bulk-order API and relies on existing on-demand dispatch.
Staff Approach: Recognizes that scheduled windows are Intent 3 — a batch routing problem. Runs a VRP planner per window 60–90 min ahead, then hands planned routes to the real-time layer for execution and exception handling. Adds vehicle capacity as a dispatch constraint. Rate-limits the partner's order API with a per-tenant quota and burst allowance. Negotiates SLAs based on measured percentiles, not averages.
Principal Approach: Evaluates whether this partner justifies a scheduled-logistics platform the company doesn't have yet, prices it (team of ~8 for a year), and decides with the business whether to build, buy a routing vendor, or decline scheduled windows in the contract.
Staff Approach — Full Reasoning
| Phase | What to Do |
|---|---|
| Scoping | Split on-demand vs scheduled volume |
| Design | VRP planner per window; capacity constraints; tenant quotas |
| Pilot | One metro, 5% of partner volume, shadow planning |
| Ramp | 25% → 100% with SLA dashboards |
| Steady state | Per-tenant SLOs, penalties tracked |
Metrics to Watch: delivery.window_hit_rate{tenant}, planner.infeasible_routes, api.tenant_throttled_total
Organizational Follow-up: Enterprise onboarding checklist; account team sees SLA dashboard.
Ownership Question: "Who signs the SLA?" Staff answer: Business owns the contract; engineering signs the achievable percentile from pilot data. Nobody signs a number we haven't measured.
Key Takeaway: "A big customer can quietly change your intent. Recognize when on-demand dispatch is the wrong tool."
What clears the Staff bar:
- Detects the intent shift
- Tenant isolation
- SLA grounded in measurement
Deep Dive 4: Post-Mortem — Riders Double-Charged After Failover#
Context: After a regional failover, ~12K riders were charged twice. Press is asking.
Questions to Surface First:
- Were two captures issued, or one capture plus a new auth converted?
- Were idempotency keys used, and were they scoped per processor account?
- Did the payment saga state replicate with the trip state?
Typical L5 Approach: Refunds duplicates and adds a dedupe check before capture.
Staff Approach: Finds that the saga state was in a region-local store not replicated to the paired region; after failover the saga restarted for trips in
COMPLETEDand generated new idempotency keys with a random component. Fix: idempotency keys are deterministic (trip_id:capture:v1) and processor-side keys dedupe regardless of region; saga state replicates with the trip store; a pre-capture check reads the processor's charge list for the trip reference.
Principal Approach: Establishes an org-wide rule: any retry-capable money operation uses deterministic idempotency keys derived from business identifiers, audited in code review; failover game days include payments. Coordinates rider communication and refunds with Comms, and reports to the finance controller.
Staff Approach — Full Reasoning
| Phase | What to Do |
|---|---|
| Immediate | Stop saga replays; identify duplicates via processor reference |
| Triage | Key generation, state replication gap |
| Quick fix | Automated refunds within 24 h with notification |
| Guardrails | Deterministic keys; pre-capture lookup |
| Post-mortem | Payments included in failover game days |
Metrics to Watch: payment.duplicate_capture_total, saga.restarted_total, payment.refund_auto_total
Organizational Follow-up: Payments failover runbook; quarterly game day.
Ownership Question: "Who decided the saga state didn't need replication?" Staff answer: Nobody decided — it was never classified. That's the fix: every store gets a data classification that determines replication.
Key Takeaway: "Idempotency keys must be derived from the business event, never generated at retry time."
What clears the Staff bar:
- Pinpoints key generation as root cause
- Replicates money state with trip state
- Adds classification to prevent recurrence
Deep Dive 5: Multi-Region Expansion — Launching Southeast Asia#
Context: Launching in 6 cities across 3 countries with different payment rails (cash common), residency rules, and motorbike taxis.
Questions to Surface First:
- Residency: must trip/location data stay in-country?
- Cash payments: how does the saga change?
- Vehicle types: motorbikes change ETA and pickup behavior.
- Network: 3G fallback, high packet loss?
Typical L5 Approach: Deploys the existing stack in a Singapore region.
Staff Approach: Region cell in-region; per-country residency where required. Cash changes the payment saga: no pre-auth; completion records cash collected; the company's commission becomes a driver debt settled from future card trips or top-ups — a new state machine. Motorbike ETA model and vehicle class. Client: lower ping frequency on poor networks, compact binary payloads.
Principal Approach: Decides build-vs-partner per market (local wallets, maps), and sets a region-launch template. Assesses regulatory posture on surge per country before launch.
Staff Approach — Full Reasoning
| Phase | What to Do |
|---|---|
| Design | Region cell; residency map; cash saga; vehicle classes |
| Pre-launch | ETA model trained on local data; payment partners integrated |
| Launch | Conservative surge caps; staffing on-call locally |
| Steady state | Per-country SLOs; driver debt reconciliation |
Metrics to Watch: driver.debt_outstanding, eta.error_p90{vehicle_class}, client.ping_loss_rate
Organizational Follow-up: Country launch checklist; local ops team ownership.
Ownership Question: "Who absorbs uncollected driver debt?" Staff answer: Finance sets the write-off policy; Risk sets debt limits before a driver is paused.
Key Takeaway: "A new payment rail isn't a config change — it's a new state machine."
What clears the Staff bar:
- Payment-rail-driven design changes
- Vehicle-specific models
- Regulatory review before launch
9. Level Expectations Summary#
After studying this case study, you should be able to:
- Classify ride-hailing data by loss tolerance and design three planes accordingly
- Size location ingestion with adaptive ping rates
- Prevent double assignment with leases plus a conditional commit
- Draw the trip state machine including cancellations, no-shows and payment settlement
- Design the payment handoff as a saga with deterministic idempotency keys
- Explain surge as a smoothed, capped control loop with a legal owner
- Explain how delivery changes dispatch timing
- At L7: price the planes, draw platform vs product ownership, and plan the 3-year evolution
The Bar for This Question#
Mid-level (L4): Rider/driver/trip tables, nearest-driver query, charge at end. Works for one city of 100 drivers.
Senior (L5): Adds geohash/H3, Redis for location, Kafka, sharding. Knows about idempotency. Keeps payment synchronous, uses a status column, and misses double assignment or solves it with a global lock.
Staff+ (L6): Separates planes by loss tolerance, prevents double assignment structurally, models the trip as a state machine with outbox and payment saga, treats surge as a controller with policy owners, and uses the city as the unit of sharding and failure. Every failure has a metric and an owner. The interviewer should learn something from the answer.
10. Staff Insiders: Controversial Opinions#
10.1 "Don't Replicate Driver Location"#
| Evidence | Implication |
|---|---|
| Pings arrive every ~4 s | Any lost state is rebuilt in one interval |
| Replicating 250K writes/s costs real money | Paying for durability the protocol already gives |
| Dispatch filters stale drivers anyway | Freshness matters more than durability |
The Staff position: In-memory latest location; history async for analytics.
Why this matters in interviews: It shows you reason from the protocol, not from habit.
10.2 "Batched Matching Is Overrated Outside Dense Cores"#
| Evidence | Implication |
|---|---|
| Batching gains come from contention between nearby requests | Sparse areas gain ~nothing |
| Every rider pays the batch window | Latency tax everywhere |
| Solvers add a hot spot and a failure mode | Operational cost |
The Staff position: Adaptive: batch where density justifies, greedy elsewhere.
Why this matters in interviews: Candidates who say "Hungarian algorithm" confidently rarely say where not to use it.
10.3 "Surge Is a Legal Feature, Not a Pricing Feature"#
| Evidence | Implication |
|---|---|
| Surge during emergencies has drawn regulatory action and public commitments to cap it | Legal constraints shape the algorithm |
| Riders compare prices across neighboring blocks | Spatial fairness is a trust issue |
The Staff position: Caps, overrides and audit trails are first-class requirements.
Why this matters in interviews: Naming the non-engineering owner is a Staff-level signal.
10.4 "The Trip State Machine Deserves the Most Expensive Database You Have"#
| Evidence | Implication |
|---|---|
| Trip writes are ~1% of total write volume | Cost of a premium store is small in absolute terms |
| Trip inconsistencies cost support, refunds and trust | Correctness pays for itself |
| Uber publicly moved fulfillment to a strongly consistent store | Industry signal |
The Staff position: Cheap stores for location; strongly consistent store for trips.
Why this matters in interviews: Spending asymmetrically is the Staff move.
10.5 "Driver Earnings Must Never Depend on Rider Payment Success"#
| Evidence | Implication |
|---|---|
| Drivers can't control rider cards | Coupling is unfair and drives churn |
| Collection risk is diversifiable for the company, not for a driver | Business should absorb it |
The Staff position: Credit at completion; company owns collection.
Why this matters in interviews: It shows you name who pays.
11. The Principal Lens (L7)#
Why L7 Sees This Problem Differently#
A Staff engineer designs a ride-hailing system. A Principal engineer sees a fulfillment company: rides, food, grocery, parcels and freight all assign mobile supply to located demand, track it, price it and pay it. The expensive, slow-to-build assets — the location plane, the road graph and ETA models, the payment and payout rails, the trip/order state machine framework — are shared, and every product team will try to fork them to move faster. The L7 question is how to keep one ETA, one location truth and one money path while letting products compete on dispatch policy and UX — and which decisions (city-as-cell, trip store technology, event contracts) will lock the company in for five years.
The Org-Level Fault Line#
One fulfillment platform vs per-vertical stacks.
| Option | What Works | What Breaks | Who Pays |
|---|---|---|---|
| Per-vertical stacks (rides, eats, grocery each build their own) | Speed for each vertical early on | ETAs disagree between apps; drivers doing both get double offers; 3× location infra; three payment integrations | Drivers, Finance, customers comparing ETAs |
| Monolithic shared platform | One truth | Every vertical waits on one team; delivery-specific needs (prep time, batching) get starved | Product velocity |
| Shared primitives + vertical policy (L7 default) | Location, ETA, leases, state-machine framework, payments as platforms; dispatch objectives and state definitions per vertical | Contract design and versioning overhead; platform funding | Platform headcount (~30–50 across teams at scale) |
🧭 Principal Move: "The platform owns anything where two versions of the truth would embarrass us in front of a customer — ETA, location, money. Verticals own anything that is their competitive difference — dispatch objective, batching, UX. The lease store is global per driver because the driver is a shared resource."
Cost Model#
Assumptions: cloud list prices order-of-magnitude; adaptive pings averaging ~5 s; ~150-byte pings; strongly consistent trip store; loaded engineer ~$250K/yr.
| Scale | Infra $/month | Dominant Cost | Headcount (eng) | On-Call Load |
|---|---|---|---|---|
| 1 city, ~5K online drivers, ~50K trips/day | ~$20–40K | Single Postgres/PostGIS, app servers, maps API fees | 6–10 in one team | 1 rotation, a few pages/week |
| 50 cities, ~100K online drivers, ~2M trips/day | ~$400K–1M | ETA/routing compute, location ingestion, maps licensing | 60–120 across ~8 teams | Per-plane rotations |
| Global, ~1M online drivers, ~25M trips/day | ~$5–15M | ETA compute (~30%), location + Kafka (~25%), trip store (~15%), data/ML | 500+ across platforms and verticals | Per-plane + per-region; follow-the-sun |
The line that moves the business case: ETA lookups per dispatch. A candidate-set or batch-size change can move infra cost by double-digit percent — ETA request volume should be a budgeted, reviewed quantity.
The 3-Year Evolution Path#
One-Way Doors vs Two-Way Doors#
| Decision | Reversibility | Cost to Reverse | Why |
|---|---|---|---|
| City/cell as the shard and failure unit | One-way | Re-architecture of every plane | All data placement follows it |
| Trip event schema published to 20+ consumers | One-way-ish | Multi-quarter deprecation | Consumers depend on fields forever |
| Trip store technology | One-way-ish | Live migration of the most critical data, 6–12 months | Keep an abstraction layer |
| Driver earnings decoupled from rider payment | One-way (trust) | Reversing is a driver-relations crisis | Commitments to supply are sticky |
| Ping interval / adaptive policy | Two-way | Client release | Tune freely |
| Batch vs greedy per zone | Two-way | Config | Experiment freely |
| Surge curve | Two-way technically, one-way reputationally | Headlines | Legal review |
The Standard I'd Write#
RFC: Fulfillment State & Money-Movement Standard (v1)
Scope: Every service that creates or transitions a trip, order or delivery, or that moves money as a result.
Mandatory requirements:
- Fulfillment entities MUST be modeled as explicit state machines in the shared framework; transitions MUST be versioned and validated server-side.
- State changes MUST emit events via a transactional outbox; direct dual writes to a database and a broker are prohibited.
- Money-moving calls MUST use deterministic idempotency keys derived from
(entity_id, step, version).- Money-moving consumers MUST NOT skip events on error; they halt and page.
- A supply unit (driver/courier) MUST hold at most one active offer across all verticals, enforced by the global lease service.
- Location history SHOULD be downsampled outside active trips and deleted after the retention set by Privacy.
Exceptions: Filed with Fulfillment Platform and Payments; time-boxed to one quarter.
Success metrics: 0 double assignments per quarter; 0 duplicate captures; earnings reconciliation mismatch = 0 for 30 days; 100% of verticals on the shared state framework within 4 quarters.
What I'd Tell the VP#
"Our riders and drivers only notice three things: how fast they're matched, whether the price makes sense, and whether the money is right. Today each of our apps builds its own version of those systems, which is why ETAs sometimes disagree and why a failover double-charged customers last quarter. I'm proposing one fulfillment platform that owns location, ETAs, trip state and money movement, with each product team free to design its own dispatch rules on top. It costs roughly 30 engineers we mostly already have, reorganized. The payoff is fewer money incidents, faster launches of new verticals, and one set of numbers our drivers can trust."
Principal Interview Signals#
| Signal | What It Sounds Like |
|---|---|
| Portfolio view | "Rides, eats and grocery share supply. The lease is per driver, not per product." |
| Prices the plane | "ETA lookups per dispatch are our biggest cost lever — I'd budget them." |
| One-way doors | "City-as-cell is the decision everything else inherits. I'd make it deliberately." |
| Writes the standard | "Outbox mandatory, deterministic idempotency keys, money consumers never skip." |
| Supply-side governance | "Driver earnings are decoupled from rider payment — that's a commitment, not a config." |
Staff answers that L7 interviewers find insufficient:
- "We'll use an outbox." — Correct, but doesn't make it the org standard or govern the event schema consumed by 20 teams.
- "Surge has caps." — Correct, but doesn't say who owns the caps across jurisdictions or how emergency commitments are enforced.
- "Delivery can reuse dispatch." — Correct, but doesn't address shared supply, cross-vertical priority, or who arbitrates.
Appendices
Appendix A: Mechanics in Depth#
A.1 Candidate Generation#
cells = h3.k_ring(h3.cell(pickup, res=9), k=ring_for(radius)) # k=5 ≈ 1.5 km
candidates = [d for c in cells for d in index[c]
if d.status == 'available' and now - d.ts < 20s
and d.vehicle_class in product.classes]
candidates = top_n_by(haversine, candidates, 100) # cheap prefilter
etas = eta_service.matrix(candidates, pickup) # one batched call
ranked = sort_by(eta + deadhead_penalty + fairness_adj, candidates)[:30]
A.2 Batched Assignment#
For a zone every 2–3 s: build cost matrix C[r][d] = eta(d, r) + penalties; add dummy columns with a high "wait for next batch" cost; solve min-cost assignment (Hungarian O(n³) is fine for n ≤ ~200; beyond, partition by zone). Requests unassigned roll into the next batch; after ~3 batches, fall back to greedy with a widened radius.
A.3 Surge Smoothing#
Temporal EWMA with α ≈ 0.3–0.5 over 60–120 s cycles; spatial averaging across the k=1 ring; hard clamp by policy cap; emergency override sets cap = 1.0 for a polygon in < 60 s.
Appendix B: Keys and Data Model#
| Entity | Key | Store | Notes |
|---|---|---|---|
| Driver latest location | driver_id in city shard memory, indexed by H3 res 9 | In-process | Rebuilt from pings |
| Offer lease | offer:{driver_id} | Redis per city (global per driver across verticals at L7) | SET NX PX 15000 |
| Trip | trip_id (embeds city) | Strongly consistent store sharded by city | Versioned |
| Driver state | driver_id co-located with city shard | Same store as trips | Single-shard transactions |
| Outbox | (trip_id, version) | Same transaction as trip | Relay to Kafka |
| Payment intent | trip_id | Payments ledger | Deterministic keys |
| Surge snapshot | (city, version) → hex multipliers | Cache + log | Quotes embed version |
| Location history | (city, date, driver_id) | Object store via Kafka | Downsampled |
Appendix C: Coordination Mechanisms — Quick Comparison#
| Mechanism | Latency | Guarantees | Failure Behavior | Use |
|---|---|---|---|---|
Redis SET NX PX lease | < 1 ms | One offer per driver while Redis healthy | Lease lost on failover → possible double offer, caught by commit | Offers |
| Conditional transaction on trip + driver rows | 5–20 ms | Linearizable per shard | Rejects on conflict | Accept, transitions |
| Single-writer zone dispatcher | < 1 ms | No conflicts within zone | Zone failover pauses offers ~seconds | Very high density |
| Global 2PC | 50+ ms | Cross-shard | Blocks on coordinator failure | Avoid |
| Workflow engine timers | seconds | Durable timeouts | Engine is a dependency | No-show, matching timeout |
Appendix D: API Contract and Client Behavior#
- Trip creation requires
idempotency_key; duplicates return the existing trip. - All trip commands carry
version; stale versions return 409 with current state. - Driver socket: server sends offers with
expires_in; client must respond within it; late accepts returnoffer_expired. - Reconnect: jittered exponential backoff (base 1 s, cap 30 s); on reconnect send only the latest location; fetch current trip state by version.
- Rider tracking: server pushes driver position every 1–2 s during approach; client interpolates.
- Quotes: valid until
expires_at; trip creation with an expired quote returns a fresh quote for confirmation.
Appendix E: Observability#
E.1 Core Metrics#
| Metric | Why |
|---|---|
dispatch.assign_latency_p50/p99{city} | Core promise |
dispatch.double_offer_total, trip.assign_conflict_total | Assignment invariant |
location.staleness_p99_s{city} | Location plane health |
trip.transition_errors_total{from,to} | Illegal or failed transitions |
outbox.relay_lag_s | Event pipeline health |
payment.capture_failure_rate, payment.duplicate_capture_total | Money correctness |
earnings.reconcile_mismatch_total | Silent money failure |
surge.multiplier_volatility{hex} | Controller stability |
E.2 Critical Alerts#
| Alert | Threshold | Severity |
|---|---|---|
| Double offer | > 0 in 5 min | Page dispatch |
| Assignment p50 | > 3× baseline for 5 min in a top-20 city | Page dispatch |
| Location staleness | p99 > 15 s for 3 min | Page edge |
| Outbox lag | > 60 s | Page trip platform |
| Duplicate capture | > 0 | Page payments (SEV2) |
| Earnings mismatch | > 0 daily | Page earnings |
E.3 Control Plane vs Data Plane#
Data plane: pings, offers, transitions, captures. Control plane: surge curves and caps, dispatch parameters (batch window, candidate caps), city-to-region mapping, ping policies. Control-plane changes are versioned, canaried by city, and reversible within one cycle.
Appendix F: Scale Evolution#
| Scale | What Works | What You Add |
|---|---|---|
| < 10K online drivers | One Postgres + PostGIS; greedy dispatch; row locks | State machine + outbox |
| 10K–100K | Redis GEO per region; leases; Kafka; payment saga | Adaptive pings, surge controller |
| 100K–1M | In-memory location per city shard; city cells; paired regions | Batched dispatch in dense cores |
| 1M+ | Fulfillment platform; global driver leases; region-in-a-box | Cross-vertical marketplace policy |
F.1 What You Don't Build on Day One#
- Batched matching
- In-process location sharding
- Multi-region active failover
- Custom routing engine (use a maps provider)
- Delivery-specific dispatch
What you do build on day one: the explicit trip state machine with outbox, deterministic idempotency keys for money, and driver earnings decoupled from rider payment.
Appendix G: Multi-Tenancy, Fairness and Cost#
- Driver fairness: track trip distribution and earnings per online hour across drivers in a zone; dispatch adds a small fairness adjustment for drivers idle longest.
- Rider fairness: monitor pickup ETA by neighborhood; dispatch radius policies should not systematically exclude lower-density areas.
- Tenant isolation (enterprise delivery partners): per-tenant API quotas and burst allowances; per-tenant SLO dashboards.
- Cost attribution: cost per trip by plane and by vertical, reported monthly; ETA lookups per dispatch budgeted.