A team ships the ride-sharing design from the previous lesson. It indexes by place, ranks by road and claims once. For a month it works.
Then a concert ends and ten thousand people open the app. The routing service times out. A card is charged twice after a retry. A reviewer asks who can see where a driver was last Tuesday. An interviewer, having heard the happy path, says the sentence that fills the last twenty minutes: “Now what breaks?”
A design is not finished until you can say how it fails, what the money does, and who can see the map.
- The working design from Design a Ride-Sharing Service: the geo index, batch matching, the claim and the ride state machine.
- Design a notification service and its follow-ups, for the same habit of a failure table and home regions.
Before You Start#
Why is the Redis claim not enough to stop a double booking? Redis replicates asynchronously, so a failover can lose a claim. The accept is a conditional write on the durable ride record, and only one passes.
- Walk a failure table component by component: what dies, what riders see, and the answer already designed in. When the claim store or ride store is down, stop offering rather than risk a double booking.
- A hot cell is a demand problem, not a matching-speed problem: batch, queue riders on the server so apps stop re-requesting, use fixed pickup points and give an honest wait.
- Smooth the surge multiplier, cap it, and lock the price the rider was shown.
- Authorize on request, capture once at completion, and key every payment call on the ride id, with your own ledger row behind it.
- Location is personal data: share it only as far as the trip needs, keep trails for a limited time and log access.
Defend a working ride-sharing design in the second half of a system design round: name what fails and what already handles it, absorb a stadium crowd, place the service in regions, keep surge stable, charge a rider exactly once, protect location data, set SLOs and answer the usual follow-ups within a 45-minute plan.
Predict first. The routing service is down. Should the matcher stop offering rides, or fall back to something? Decide before you read the first rows of the table below.
What Breaks, and What Do You Watch?#
| If this dies | What riders see | Answer |
|---|---|---|
| Location index (one region) | Few or no cars shown; matches pause | Drivers re-report within one interval (4 s), so the index rebuilds in seconds; a standby process takes over |
| Routing service | Slower or worse ETAs | Fall back to straight-line distance times a detour factor (judgement), mark the ETA as approximate; do not stop matching |
| Traffic feed | ETAs ignore live traffic | Use stored typical speeds by hour; alert on feed age |
| Claim store (Redis) | Offers cannot be made safely | Pause offers, keep riders in requested; the ride record stays the truth |
| Ride store | Cannot create or change rides | Return an error so apps retry with the same request_id; never accept without a stored record |
| Matcher instance | Offers in flight stall | Another instance resumes from stored offered rides past expires_at |
| Gateway (held connections) | Apps reconnect | Resume from the last event number; apps fetch state with GET |
| Push provider | Nudges are late | Held connections and polling still show the state |
| Payment provider | Capture fails | Retry with the same idempotency key; the trip is not blocked |
The rule behind the table (judgement): fail toward safety for the claim and the money, and toward a worse answer for the ETA. A rough ETA still gets a rider home; a duplicate claim or duplicate charge does not.
What Do You Watch?#
Watch these signals, each with a percentile or a rate, because averages hide a slow tail:
- Time to match, from request to accepted offer, at the median and the 95th percentile.
- Offer accept rate and claim conflict rate. A drop in accepts means drivers are being offered bad rides; a rise in conflicts means a hot cell.
- Location staleness: the age of the oldest driver entry still counted as online.
- Routing latency at the 99th percentile, and the age of the traffic feed.
- Double bookings: a count that must be zero. Any non-zero value pages a person, whatever the rate.
An SLO turns these into promises. Assume (a made-up target) that 95% of requests are matched, or told “no cars” honestly, within 10 seconds, and location staleness at the 99th percentile stays under 10 seconds. Alert on how fast the budget is being used, not on every blip.
Cost and Rollout#
This section is design judgement, not measured fact.
- Routing is likely the largest per-request cost, so cache ETAs between pairs of cells and call the matrix once per batch, not once per candidate.
- Gateways scale with connections: at an assumed 50,000 per node, 500,000 drivers need 10 nodes, and 20 with room to fail over.
- Rollout goes city by city behind a flag. Run the new matcher in shadow (decide, do not offer), compare time to match and pickup ETA with the old one, and keep a switch back to nearest-first.
What Happens When a Stadium Empties?#
Splitting by city does not help when the crowd is in one corner of one city. The load follows the event, not the shard map.
Assume 10,000 riders ask within five minutes, near about 1,000 drivers. That is 10,000 / 300 s ≈ 33 requests a second, so the matcher’s speed is not the problem. Every search reads the same cells, ranks the same drivers and tries to claim them, so most claims fail.
Worse, an unmatched rider taps again. If every waiting rider re-requests every 10 seconds (an assumed behaviour), 10,000 / 10 = 1,000 requests a second, about 30 times the first number.
-
Works: Do nothing special. Every request runs the normal search and tries to claim the top driver. Trade-off: Claims collide, riders retry, and the retries become the load. Drivers in the crowd are offered rides they cannot reach through the traffic.
-
Scales: Batch, queue on the server, widen. Match in short batches so the drivers nearest to the venue are shared sensibly. Keep the rider’s request open on the server in the
requestedstate, so the app waits for a push instead of re-requesting, and the samerequest_idmakes any retry harmless. Widen the search ring to bring in drivers from farther away, and apply client backoff with random jitter. Trade-off: A batch and a queue add a wait. A wide ring brings in drivers who need longer to arrive. -
Survives: Pickup points, supply nudges and an honest wait. Move pickups to a few marked points so cars are not stuck in the crowd, and answer each rider quickly with a wait estimate based on queue length. Encourage drivers to head toward the area before the event ends (a product and pricing decision). Limit concurrent searches per cell, and drop to coarser results when the cell is saturated. Trade-off: Fixed points mean a walk. Nudging supply costs money, and admission control must still tell the rider the truth.
Where Does It Run?#
Each city is a shard with its own location index, claim store and ride store. A ride lives and dies in one city.
-
Works: One region, restore elsewhere. Everything runs in one region with backups elsewhere. Trade-off: A region outage stops every city in it until the restore ends.
-
Scales: Each city has a home region. Cities are spread over regions, so one failure affects only its cities. Rides never cross cities, so ride state needs no cross-region writes. A rider near a border reads two city indexes. Trade-off: A hot city cannot borrow capacity from another region. Over-provision the largest cities.
-
Survives: Declared failover, one writer per city. Copy each city’s ride events to a standby region. On failure, declare a failover and let the standby become the only writer. Replication is asynchronous, so the last seconds of events can be missing: Redis’s own documentation says acknowledged writes can be lost during a failover, and a database copy behaves the same. After failover the new region rebuilds the live index within one location interval, and apps re-fetch ride state. Trade-off: Synchronous copying would remove the gap at the cost of a cross-region delay on every transition. For rides, a rare lost last transition (the app shows the old state and the driver re-taps) is usually cheaper. In CAP and PACELC terms the live index and pushes choose availability, while the ride record stays single-writer and consistent.
A Redis detail that matters for hot cities: a Redis Cluster spreads keys over 16,384 hash slots, and one geo key lives in one slot on one node. Shard the index by cell, such as one key per cell prefix, or a single busy city key becomes the hot node (judgement).
How Does Surge Stay Stable and Fair?#
Pricing is a product decision, but the system still has to compute and apply it. Uber says surge goes into effect when there are more riders in an area than available drivers, with driver availability and rider demand as its primary inputs, and that its teams cap surge in an emergency. Its H3 post says supply and demand are measured per hexagon in each city.
A sketch with made-up numbers: every 60 seconds, per cell, count open requests (demand) and free drivers (supply).
raw = demand / supply
= 120 / 80 = 1.5
smoothed = 0.7 x old + 0.3 x raw
old 1.2 -> 0.7x1.2 + 0.3x1.5
= 0.84 + 0.45 = 1.29
capped at 2.5 (assumed)Smoothing stops the number jumping from 1.2 to 2.0 and back; neighbouring cells can be blended so riders do not see a price cliff across a street. Lock the quoted price when the rider requests, with an expiry, so the fare does not change under them.
The fairness questions are judgement, and an interviewer will probe them: the same price should apply to riders with the same trip at the same moment; emergencies need a cap (as Uber describes); the multiplier should be explained in the app; and higher prices should be a signal to bring in drivers, not a way to hide a shortage.
How Is a Rider Charged Exactly Once?#
The same lesson as the request id: a retry must find the earlier result, not repeat the work.
The flow, in judgement terms: authorize a hold when the ride is requested, capture once when the ride completes, release what was not used. A card hold is built for this: authorize now, capture later, and a partial capture releases the rest. Online card holds usually last about 7 days, with some shorter windows by card network, far longer than a ride.
The danger is a timeout after the provider charged but before you heard back. Send every payment call with an idempotency key derived from the ride, such as ride_id:capture. Stripe, for example, saves the first response for a key and returns it on a retry, rejects the same key with different parameters, and may prune keys after 24 hours.
Because the provider’s window is limited, keep your own ledger: one row per ride and per payment step, written in the same transaction as the completed transition. A retry job reads the ledger, not the provider, and a nightly job compares your ledger with the provider’s records.
| Failure | What happens | Why it is safe |
|---|---|---|
| Timeout on capture | Retry with the same key | Provider returns the first result |
| App sends two “end trip” | One completed transition | The version check rejects the second |
| Retry after 24 h | Key may be pruned | The ledger row says captured; no call is made |
| Capture fails (card declined) | Ride is already done | Mark the fare owed, retry later, notify the rider |
Who Can See Where Everyone Is?#
Location is personal data, and a trail of it says where someone lives and works. These are design rules, not legal advice:
- Share as little as the trip needs. A rider sees a driver’s position only after a match; a driver sees the pickup only after an offer, and the exact drop-off only after accepting. Any other rider asking for the ride gets a 403.
- Keep trails for a limited time. GDPR Article 5 asks that data be collected for a specified purpose, limited to what is necessary, and kept in identifiable form no longer than necessary. Keep trip trails for disputes and safety for a set period, then delete or aggregate them.
- Log staff access to live and historical locations, and limit it by role.
- Do not trust the phone. GPS is about 4.9 m under open sky and worse near buildings, and spoofing apps exist. Reject a jump that implies an impossible speed (for example, a 300 km/h move between updates, an assumed threshold) and compare the phone’s position with network hints.
- Safety rules live in the matcher. Uber’s matching page gives one: it prevents matches if one has given the other a one-star rating in the past.
Fraud patterns to name: a driver and rider colluding for fake trips, ghost drivers who sit online without moving, and fake pickups to farm incentives. The usual defences are a velocity check on trips per account, a trip that must show a believable trail, and holding payouts for new accounts (judgement).
Follow-Ups the Interviewer Will Ask#
Expect these after the main design; back each answer with a trade-off and a number.
| Follow-up | Strong short answer |
|---|---|
| 10x traffic (≈ 1.25 million updates a second) | Shard the index by cell prefix, add gateways, lengthen the interval when idle. Updates are the load; matching stays small |
| A driver’s phone loses signal mid-trip | Keep the trip; mark the driver stale after the timeout; the trail has a gap and the fare uses the route estimate |
| Rider cancels right after a match | A conditional transition: allowed from matched, the offer is released, the driver is freed, a fee is a product rule |
| Driver accepts as the offer expires | Both are conditional writes on one version; one wins, the other gets 409 |
| A rider has two phones | The ride is keyed by rider; both apps subscribe to the same events |
| Airports with queues | A virtual queue per zone; the matcher offers in queue order |
| Drivers who want trips toward home | Add a destination filter to the candidate list. Uber says it gives drivers the choice to receive trips toward a destination they set in advance |
| Pooled rides | Out of scope; matching becomes a route problem, not a pairing |
| Accept rate suddenly drops | Check offer quality and the routing ETA first; compare per cell |
Cover the right-hand column of the table and say each answer aloud in one breath. Do it again tomorrow without the cover.
The 45-Minute Plan#
Spend the round in this order: 5 minutes for requirements and assumptions, 5 for numbers, API and data model, 10 for the simple design and the geo index, 15 for deep dives (ingest, matching and the claim, ride state), 5 for failures and consistency, and 5 for surge, payments, privacy and wrap-up. If the interviewer pulls you into one deep dive, shorten the others rather than skipping failures.
Try It: A Stadium Empties#
A concert ends and 10,000 riders ask for a ride within 5 minutes, in an area with about 1,000 drivers. Assume each driver finishes one ride, including the pickup, in 20 minutes, and no new drivers arrive.
- What is the request rate, and what is it if each unmatched rider re-requests every 10 seconds?
- How long to serve everyone?
- What do you change, and what do you tell riders?
Hint
Divide riders by seconds twice, then drivers by 20 minutes for a service rate. Ask what the matcher can do about the second answer.
One reasonable answer
10,000 / 300 s ≈ 33 req/s
10,000 / 10 s retry = 1,000 req/s
1,000 drivers / 20 min
= 50 rides/min
10,000 / 50 = 200 minThe retry storm, not the first 33 a second, is the real load, so keep requests queued on the server. Matching cannot fix a 200-minute backlog: supply has to come from outside, through nudges to nearby drivers and a higher price that is explained. Match in batches, widen the ring, use a few pickup points and show an honest wait. Pricing is a product decision outside this lesson.
Check Yourself#
1.The routing service is down. Why keep matching instead of pausing?
A rough ETA from straight-line distance and a detour factor still gets a rider matched, and the ETA is labelled approximate. Pausing would stop every ride. The claim store and ride store are different: without them a double booking is possible, so offers pause.
2.A payment capture times out. Why can the retry not charge twice?
The call carries an idempotency key derived from the ride, so the provider returns the first result. Because provider keys can be pruned after 24 hours, a ledger row per ride also records that the capture happened.
3.Why does a stadium crowd create more load than its request rate suggests?
Unmatched riders re-request. At one retry every 10 seconds, 10,000 waiting riders make 1,000 requests a second against 33 a second at first. Holding requests on the server and using the same request_id removes the storm.
In One Picture#
Failures
safe for claim and money,
rough is fine for ETA
Hot cell
batch, queue on server,
fixed pickups, honest wait
Surge
smooth, cap, lock the quote
Money
hold, capture once,
key on ride id, own ledger
Privacy
share little, keep briefly,
log accessConclusion#
A ride-sharing design earns the first half of the round by being correct. It earns the second half by being survivable: you know what fails, the claim and the money stay safe, and location is treated as data that belongs to someone.
Which failure would worry you most in a service you run: the claim store, the ride store or the payment call? Tell us in the comments.
- The design this lesson hardens: Design a Ride-Sharing Service.
- Apps that reconnect after a drop: design a chat system.
- The same habit in another problem: notification service follow-ups.

