Courses / System Design Interview / Chapter 27
Module 3 · Design Real Products · Chapter 27 of 39
Design a Ride-Sharing App
Builds on Back-of-the-Envelope Estimates, Abstractions That Hide the Network, Databases: Replication, Partitioning and Choosing a Store, Consensus, Leader Election and Coordination, Distributed Caches, Message Queues, Publish-Subscribe, Distributed Task Schedulers, Design Maps and Navigation and Design Nearby Places Search. Next up: Design a Microblog.
Part 1 · Requirements and Scale
The brief
Friday, 8 pm, Mumbai. The first heavy rain of the monsoon has stopped the local trains, and outside Andheri station Ananya opens a ride app along with thousands of other stranded commuters. Three free cars are within two kilometres of her. The best of them, Driver A, has just driven into an underpass and lost signal. Driver B, on one bar of network, will tap Accept twice. Within about 15 seconds Ananya should still see "Driver B, 5 min away", B must never be sent to two riders at once, and her ₹250 fare must be charged exactly once, however often a phone retries.
The product: a ride-sharing app for a large country. Riders see cars, autos and bikes nearby, get a price, book, watch the driver arrive and pay after the trip. Drivers go online, accept ride offers and get paid. Plan for 10 million rides a day and the evening rush. Ananya's booking, ride 812, runs through the whole chapter.
What we are building
Ananya's ride looks simple on her phone. Underneath, the nearby places chapter searched for things that stay put and the maps chapter priced roads with live traffic; a ride-sharing app is a nearby search where every result is moving, and each result can serve only one rider at a time. Three problems follow: tracking about a million moving drivers, matching each request to a good driver in seconds, and running each trip reliably through to payment.
The scope for today, by who uses it:
| In scope | Out of scope today | |
|---|---|---|
| Rider | See nearby vehicles, get a fare, book, track the driver, pay, rate | Shared rides, scheduled rides, trips with several stops |
| Driver | Go online, accept offers, navigate, start and end trips, see earnings | Sign-up, document checks, incentive schemes |
| Platform | Driver locations, matching, the trip lifecycle, surge pricing, payment | Maps and routing (the maps chapter), in-app calls and chat, fraud in depth |
Routing comes ready-made from the maps chapter, so the effort here goes into moving drivers, matching and trips. The qualities that shape the design, as numbers:
- Quick matching: most riders get a driver within about 15 seconds; everyone gets an answer within about 2 minutes.
- Live positions: the car on the rider's map is at most a few seconds behind the real one.
- One driver, one trip: never two riders for one driver, or two drivers for one rider. Correctness beats speed here.
- Nothing lost: a booked trip is never lost, and no trip is charged twice.
- Always on: 99.99% for booking, under an hour of downtime a year; an outage strands riders and costs drivers income.
- Private: each side sees the other's location only during the trip; calls use number masking, a temporary number that dies with the trip.
How big it gets
How much load does an evening of Ananyas create? Assume 10 million rides a day and 1 million drivers online at the evening peak, each driver's phone sending its position every 4 seconds, the interval the maps chapter used for navigation pings.
Try it first: 1 M drivers ping every 4 s, and 10 M rides a day peak at 3× the average. How many location updates and bookings arrive per second at the peak, and how many trips are in progress if each lasts 30 minutes?
- Pings: 1 M ÷ 4 s = 250,000 a second.
- Bookings: 10 M ÷ 86,400 s ≈ 116 a second on average, × 3 ≈ 350 at the peak.
- Trips in progress: 350 a second × 1,800 s ≈ 630,000, by Little's law (below).
The full table adds what those three numbers imply:
| Quantity | Working | Result |
|---|---|---|
| Bookings, peak | ≈ 116 /s average × 3 | ≈ 350 /s |
| Fare checks, peak | about 5 per booking | ≈ 1,750 /s |
| Location updates, peak | 1 M drivers ÷ 4 s | 250,000 /s |
| Location data in, peak | 250,000 /s × about 100 B | ≈ 25 MB/s |
| Trips in progress, peak | 350 /s × 1,800 s (30 min with pickup) | ≈ 630,000 |
| Positions pushed to riders | 630,000 trips ÷ 4 s | ≈ 160,000 /s |
| Open WebSockets, peak | 1 M drivers + about 1 M riders, at 50,000 busy sockets a server | ≈ 2 M, on about 40 servers |
Location updates outnumber bookings about 700 to 1, whether or not anyone books. The trips row uses Little's law: things in flight = arrival rate × time each one stays, which holds because the rush lasts hours, far longer than one trip.
The live driver map is tiny but restless: 1 M drivers × about 100 bytes (id, position, heading, cell, state, vehicle, time) is only about 100 MB, yet nearly all of it is rewritten every 4 seconds. Stored data stays modest: trip rows add about 10 GB a day (3.7 TB a year), route traces about 100 GB a day, and raw pings, about 700 GB a day, are kept only a few days.
Trips and payments fit an ordinary database, sharded for safety more than for size. Live locations need memory, partitioned by city, with no disk write per ping.
Part 2 · API, Data and the Big Picture
The API
With the load known, what do the two apps actually call? Riders make a few ordinary calls; drivers also stream positions and must receive offers within a second.
POST /v1/quotes { pickup, drop, vehicle: "car" } -> 200 { quoteId, fare: 250, surge: 1.25, expiresAt }
POST /v1/rides { quoteId, paymentMethod: "card" } -> 201 { rideId: 812, status: "requested" }
Idempotency-Key: one per booking, reused on retries
GET /v1/rides/{rideId} -> 200 { status, driver, vehicle, etaSec }
POST /v1/rides/{rideId}/cancel -> 200 { cancellationFee }
PUT /v1/drivers/me/availability { online: true, vehicle: "car" } -> 200
POST /v1/drivers/me/locations { points: [{ lat, lng, heading, t }], tripId } -> 202
POST /v1/offers/{offerId}/accept -> 200 { rideId }, or 409 if expired or taken
POST /v1/rides/{rideId}/events { type: "arrived" | "start" | "end", code } -> 200 { status }
WS /v1/live server pushes: offers to drivers; positions, trip status and nearby cars to ridersPositions and offers usually travel over the WebSocket; the HTTP lines show what they carry.
Quotes come first. Ananya's quoteId holds the ₹250 price for a few minutes, so the fare cannot change between seeing it and tapping Book. Booking carries an idempotency key, as in the abstractions chapter: if the rain-soaked network makes her app send the booking twice, she still gets one ride, not two drivers.
Both apps keep a WebSocket open: a two-way connection that stays up, so the server can push at any moment. Offers reach drivers in well under a second, and the rider's map moves without battery-draining polling. A session directory records which connection server holds each phone's socket, so an offer reaches the right one.
Phones drop connections all the time. An app that missed the "driver arrived" push calls GET /v1/rides/{rideId} on reconnect and catches up. A missed push should cost a few seconds, never a wrong state.
The data model
Behind those calls sit two kinds of data. Live state, where each driver is and whether they are free, changes every few seconds, is read in milliseconds and can be rebuilt from the next pings. Records, meaning trips, payments and ratings, change a few times per trip and must never be lost.
id (pk)namephoneratingdefault_paymentid (pk)namephonecity_idvehicle_typeplateratingid (pk)rider_idfaresurgeexpires_atid (pk)city_idrider_iddriver_idassignment_noquote_idstatusoffered_tooffer_expires_atpickupdropfarerequested_atcompleted_attrip_id + seq (pk)from_statusto_statusactoratid (pk)trip_id (unique)amountmethodstatusprovider_refrider_id + completed_at (pk)trip_idcity_idfaresummary- trips rider_id → riders
- trips driver_id → drivers
- trips quote_id → quotes
- trip_events trip_id → trips
- payments trip_id → trips
- rider_trips rider_id → riders
The relational trips table is sharded by city, because matching, trips and surge all stay inside one city, and trouble on Mumbai's shard leaves Delhi's alone. Load is not the reason: even the busiest city needs about a thousand writes a second at peak. payments.trip_id is unique, so no trip can hold two payments. A consumer of trip events copies each finished trip into rider_trips, keyed by rider, so "Your trips" reads one partition instead of every city's shard.
The live state is three kinds of key:
| Live key | Holds | Lifetime |
|---|---|---|
driver:{id} | Position, heading, cell, state (free, offered, on trip), trip id, vehicle, last ping time | Expires 30 s after the last ping |
cell:{geohash}:{vehicle} | Ids of the free drivers in that length-6 cell | Changes when a driver changes cell or state |
surge:{area} | The current multiplier for the area | Replaced every minute |
These keys live in an in-memory key-value store, partitioned by city, each driver: key with a TTL (time to live), as in the distributed cache chapter. Losing them is fine: every online driver pings again within seconds. The expiry doubles as the offline detector. A dead phone or a long tunnel stops the pings, and the entry vanishes after 30 seconds, like an expiring lease in the task scheduler chapter.
The big picture
Now follow ride 812 through the whole system. Four services do the main work. Connection servers hold every phone's WebSocket and pass pings to the location service, which writes the geo index. The matcher finds, ranks and offers. The trip service owns each trip's state and the trips database. Payments and surge pricing sit to the side.
Predict before you watch: the matcher offers ride 812 to Driver A, whose phone lost signal a second ago. What does Ananya see, and what happens when A's phone wakes up and A taps Accept late?
- She sees "finding your driver" until A's offer expires after 15 seconds; then Driver B gets the offer.
- A's late Accept changes nothing: the ride is no longer offered to A, so A is told it has gone.
Watch for the 15-second offer: Driver A's phone has just gone dark in an underpass
Read it as steps
Driver A's phone is dark, so the offer runs out its 15 seconds; A's late Accept then fails because the ride is already B's.
Two rules saved ride 812. Every offer has a deadline, so a dead phone cost Ananya 15 seconds rather than her booking, and every accept is a conditional update, so A's late tap could not take a ride that was already B's. The deep dives build each piece in turn: the index, the ranking, the offer race and the trip's states.
Part 3 · Deep Dives
Tracking a million moving drivers
This is the part an ordinary database cannot do: 250,000 location writes a second into an index that can be searched by area.
How did the matcher find A, B and C in milliseconds? The nearby places chapter indexed things that almost never move; drivers turn its numbers upside down:
| Nearby places | Nearby drivers | |
|---|---|---|
| Items | 200 M places | ≈ 1 M online drivers |
| Changes | ≈ 2 a second | ≈ 250,000 a second |
| Index size | ≈ 8 GB | ≈ 100 MB |
Over 100,000 times the churn rules out batch rebuilds: refreshing every minute leaves a car 500 m behind at 30 km/h. So the index lives in memory and changes ping by ping:
- 1Arrivethe connection server gets a small batch of positions from the driver app
- 2Overwriteset
driver:{id}to the newest position, expiring in 30 s - 3Check the cellwork out the length-6 geohash; if it is unchanged, the usual case, skip step 4
- 4Change cellsotherwise move the driver from the old cell's set to the new one
- 5Pass it onpush it to the rider if on a trip; append it to the ping stream for traces and traffic
Step 4 is rare. A car at 30 km/h covers about 33 m between pings, so crossing a length-6 cell, about 1.2 km × 0.6 km, takes 18 to 36 pings. Almost every ping is one overwrite in memory, which is why a handful of machines absorb 250,000 a second.
Two races need care. A batch delayed by a weak network can arrive after a newer one, so the overwrite applies only if its timestamp is newer; otherwise the car jumps backwards. And a cell move is two writes, so a crash between them can leave a stale id in a cell. That is harmless, because the cell sets are only an index: the matcher re-reads each driver:{id} entry, the truth, and skips ids whose entry is missing or elsewhere.
Partition by city. Matches rarely cross city lines, so each city gets one shard with a replica in another zone; a pickup near a boundary asks both. Mumbai, with perhaps 100,000 drivers online, sends 25,000 pings a second, easy for one in-memory node; the largest cities split further by cell prefix.
Showing nearby vehicles
Before Ananya booked, her home screen showed cars and autos crawling around the station. It looks like decoration, yet it is the busiest read in the system, so size it like any other feature.
Assume about 1,750 riders a second open the app at the peak, as many as there are fare checks, and keep the map open for about 80 seconds. By Little's law, about 140,000 maps are open at once; refreshed every 4 seconds, that is about 35,000 refreshes a second.
Done naively, each refresh reads the 3 × 3 block of cells for every vehicle type, then every driver entry in them, about 170 in a busy area: roughly 6 million key reads a second, 24 times the 250,000 pings that write the index. Compute each cell's picture once, and push it over the socket the rider already holds:
- 1Subscribethe app subscribes to the cells on its screen over its WebSocket, and changes them as the map moves
- 2Snapshotevery 2–3 s, a builder reads each watched cell once: at most 10 vehicles per type, positions rounded to about 50 m, no driver ids
- 3Pushconnection servers forward each new snapshot to the cell's subscribers; the app animates icons between snapshots
Riders cluster in cities. A city of 1,000 km² has about 1,400 length-6 cells, so even fifteen busy cities make only about 20,000 watched cells. One snapshot each every 2.5 s is 8,000 snapshots a second; at about 20 entries each, roughly 160,000 key reads a second instead of 6 million. Each open map receives one small bundle every 2.5 s, about 56,000 pushes a second, and rounding plus missing ids mean nobody can follow one driver from the home screen.
The cars on the home screen are a hint. Quotes and the matcher always read the live index, so a stale icon can never cause a wrong match.
Finding the right driver
Back to ride 812: which of the free cars should get the offer? Matching starts as a nearby search: free drivers of the right type in the pickup's cell and its eight neighbours, the 3 × 3 block from the nearby places chapter, which covers at least 600 m in every direction. Drivers who have missed two pings in a row are skipped, since they would likely miss the offer too. If fewer than about 10 turn up, the search widens ring by ring, up to a few kilometres.
Then it ranks by pickup time, not distance, because straight lines lie in a city. The matcher asks the maps chapter's routing service for each top candidate's driving time; one search from the pickup, following roads in reverse, reaches them all at once. Here is how Ananya's three candidates compare:
| Driver | Straight line | Driving time to pickup | Rank |
|---|---|---|---|
| A | 1.5 km | 4 min: on the same main road | 1 |
| B | 1.1 km | 6 min: two signals and a U-turn | 2 |
| C | 0.6 km | 12 min: across the railway, nearest bridge 3 km away | 3 |
Straight line1.5 km
Driving time to pickup4 min: on the same main road
Rank1
Straight line1.1 km
Driving time to pickup6 min: two signals and a U-turn
Rank2
Straight line0.6 km
Driving time to pickup12 min: across the railway, nearest bridge 3 km away
Rank3
The nearest car as the crow flies, C, is the worst choice. Other signals then adjust the order: heading towards the pickup beats heading away, and a fairness rule stops one driver getting every ride.
Matching each request the moment it arrives, greedy matching, is simple but wasteful when requests bunch up. Suppose rider R1 asks first and drivers X and Y are free: X is 2 minutes from R1 and 3 from R2; Y is 3 minutes from R1 and 9 from R2. Greedy gives R1 the nearest driver, X, and leaves R2 waiting 9 minutes: 11 in total. Batch matching collects an area's requests for a second or two and solves an assignment problem, pairing riders and drivers for the lowest total: R1 gets Y and R2 gets X, 6 minutes in total and nobody waits more than 3. The price is a short delay before every match, worth paying outside a station at rush hour, not on a quiet night.
Offers, timeouts and races
Choosing A was half the job; the driver must still say yes. A may never answer, and two matchers may want the same driver at once. Deadlines handle the first. Compare-and-set writes, which succeed only if the value is still what the writer expected, handle the second.
Offers expire after about 15 seconds. The matcher records each offer on the trip, with its driver and deadline (offered_to, offer_expires_at), before sending it. A decline or silence moves the offer to the next driver; after a few tries or about 2 minutes, the rider hears that no driver is free. Each request is a leased job, as in the task scheduler chapter, so if a matcher crashes, another picks up the trip, still requested, and carries on.
Four races can hit ride 812, and each has a guard:
| Race | Without a guard | The guard |
|---|---|---|
| Two matchers pick B at once | B gets two offers | Compare-and-set on driver:B in the in-memory store: free becomes offered only if still free |
| Ananya cancels as B accepts | B drives to an empty corner | Accept needs the trip still requested; a later cancel is pushed to B at once |
| B taps Accept twice | Two assignments, or an error | The repeat finds B already assigned and gets the same answer |
| A accepts after the deadline | A takes a ride now offered to B | The update requires A's offer to be live, so nothing changes: reply 409 |
Every guard is a write that checks before it changes. In the relational trips database, the accept looks like this:
-- Driver B accepts ride 812: only while it waits and B's offer is live. UPDATE trips SET driver_id = 'B', status = 'accepted', assignment_no = assignment_no + 1 WHERE id = 812 AND status = 'requested' AND offered_to = 'B' AND offer_expires_at > now(); -- 1 row changed: B has the ride. -- 0 rows: read the trip. Already B's (a repeat)? Reply 200 again. Otherwise reply 409.
A unique index allowing each driver one active trip backs up the in-memory reservation.
The losing driver's app says "this ride is no longer available" at once, and the driver's entry goes back to free. A driver stuck on a spinner, or left marked as offered with no ride, cannot earn.
The trip state machine
Once B has the ride, what stops a flaky network from producing nonsense, such as a trip that ends before it starts? A state machine: a list of states and the only moves allowed between them.
| State | Meaning | Can move to |
|---|---|---|
| requested | Waiting for a driver | accepted, cancelled |
| accepted | Driver assigned, heading to the pickup | arriving, cancelled; back to requested if the driver drops out |
| arriving | Driver at or within about 100 m of the pickup; free waiting time runs | on trip, cancelled |
| on trip | Rider in the car, after the driver enters the rider's 4-digit code | completed |
| completed | Rider dropped; payment follows | nothing: final |
| cancelled | Ended before the ride began, by a cancel or no driver found | nothing: final |
Only two states are final, and every other state has a way out, so no trip can get stuck. The 4-digit code proves the right rider is in the right car. Here is ride 812 end to end:
Watch for the lost reply in the underpass: Start arrives twice, the state moves once
Read it as steps
Only the trip service moves the state, so a resent Start changes nothing and the capture, keyed by the trip id, happens once.
B's app sent Start twice, yet the trip moved once, because each move is a conditional update like the accept: UPDATE trips SET status = 'on_trip' WHERE id = 812 AND status = 'arriving'. A repeat finds the trip already moved and gets the current state back; a forbidden move, such as ending a trip that never started, is refused.
Each move and its trip_events row commit in one transaction, and a relay publishes new rows to payments, receipts and ratings (the pub-sub chapter). This is the outbox pattern from the message queue chapter: a crash after saving cannot lose the event.
When an accepted driver goes silent
The state machine covers moves that apps ask for. Some moves have no message to trigger them: a trip stuck in requested for 2 minutes must become cancelled, "no driver found". A timer service, like the delayed tasks in the task scheduler chapter, fires these moves, so no trip waits forever.
The hard case is a driver who accepted and then went silent on the way. Suppose B's phone dies two streets from Andheri station. Waiting strands Ananya. Reassigning carelessly can send two drivers to one rider, or let B's late "arrived" land on the new driver's trip.
- 1Noticethe timer fires: the trip is still accepted, but B has sent no pings for 2 minutes
- 2Void B's assignmentone conditional update sets the trip back to requested and clears the driver, only if it is still accepted by B
- 3Tell the rider"Your driver is not responding, so we are finding you another." No fee, and the trip goes first in the next match
- 4Match againB is left out of this trip's search and stays off the free list until B pings again
- 5Fence the old driverevery driver message carries the assignment number; the next accept raises it, so anything late from B is refused
If B's phone comes back just as the timer fires, the conditional update settles it: whichever lands first wins, and the other finds the trip no longer in the state it expected. The assignment number is a fencing token, the idea the coordination chapter uses for locks: a message stamped with an old number can never act on a newer assignment.
Part 4 · Bottlenecks, Failures and Trade-offs
What breaks
The deep dives each handled one failure. Here they are side by side with the rest, as riders and drivers would notice them:
| Problem | Effect | Response |
|---|---|---|
| A location shard dies | That city's drivers vanish from search | The replica takes over; even a blank node refills within seconds, since pings carry the trip id, if any |
| Driver's phone dies mid-trip | No pings, no "end trip" | The trip stays on trip; queued actions replay on reconnect; trace gaps are filled from the planned route |
| A matcher crashes | Its requests stall | Its leases expire; another matcher restarts the trips still requested |
| A connection server dies | About 50,000 phones lose their socket | They reconnect through the load balancer and fetch the current state |
| A deploy restarts the fleet | All 40 connection servers at once would send 2 M phones reconnecting together | Restart 2 servers (5%) at a time; their 100,000 phones reconnect after a random delay of up to a minute, about 1,700 a second |
| A cricket match ends | Thousands of requests in a few cells | Batch matching, surge, a marked pickup zone; spread the hot cells over more nodes |
| A zone or a whole region fails | A zone: a share of every tier. A region: every city homed there | Zone: replicas in other zones take over in seconds. Region: its cities move to a standby, and bookings pause 1–2 minutes |
One pattern runs through the table: live state is rebuilt from the next pings, while records survive through replicas and conditional updates.
The real-time tier dominates the server bill: sockets, location shards and routing for ETAs are sized for the evening rush and run all day. Per-trip provider fees, for card payments and masked calls, can rival the servers.
Trade-offs
Behind those responses sit choices that could reasonably go the other way. Each row names the condition that flips it:
| Choice | Gain | Cost | Flips when |
|---|---|---|---|
| Pings every 4 s, not 2 s | Half the writes, battery and data | The car on the map lags up to 4 s | Riders watch the last few hundred metres; ping faster near the pickup only |
| Batch matching | Lower total pickup time | 1–2 s of extra wait per match | Quiet nights, when requests rarely overlap |
| Offers to one driver at a time | No lost races; fair to drivers | A silent driver costs 15 s | Drivers are scarce and waits are long: offer two or three at once |
| Many matchers with compare-and-set | Scale out; no pause when one dies | A guard on every offer; batching is harder | Batching matters more: one leader per area of a city |
| Each city homed in one region | One writer per trip, no conflicts | A region loss pauses bookings 1–2 minutes | Those pauses cost more than resolving conflicts between regions |
GainHalf the writes, battery and data
CostThe car on the map lags up to 4 s
Flips whenRiders watch the last few hundred metres; ping faster near the pickup only
GainLower total pickup time
Cost1–2 s of extra wait per match
Flips whenQuiet nights, when requests rarely overlap
GainNo lost races; fair to drivers
CostA silent driver costs 15 s
Flips whenDrivers are scarce and waits are long: offer two or three at once
GainScale out; no pause when one dies
CostA guard on every offer; batching is harder
Flips whenBatching matters more: one leader per area of a city
GainOne writer per trip, no conflicts
CostA region loss pauses bookings 1–2 minutes
Flips whenThose pauses cost more than resolving conflicts between regions
Most rows trade a few seconds of waiting for fewer races or less load, and the numbers decide which side wins.
The ping interval shows that a trade-off can adapt instead of staying fixed. The app pings every 4 s while the car moves or is on a trip, every 10 s while it is parked and free, and at once when it starts moving or receives an offer. Capacity is planned for the evening peak, when most drivers move; at night, with most parked, the same rule cuts pings by about half.
When a zone or region fails
The last row of the failure table needs a plan of its own. A 99.99% booking target allows about 52 minutes of downtime a year, 4.3 minutes a month. Losing a zone, one data centre within a region, must cost nothing visible. Losing a whole region may cost a minute or two, and only if every step is planned.
Each city is homed in one region, with a warm standby in another. Its matching, trips and payments run only in the home region, so every trip has one writer. The standby keeps an asynchronous copy of trips and payments and runs a small fleet of connection and location servers, ready to grow. Each kind of state recovers differently:
| State | If a zone fails | If the region fails |
|---|---|---|
| Live locations, in memory | A replica of each city shard in another zone takes over | Rebuilt in the standby from drivers' next pings, within seconds |
| Trips and payments | A synchronous replica in a second zone: no committed trip is lost | An asynchronous copy in the standby: the last few seconds may be missing |
| Connection servers | Spread over zones; phones reconnect to the rest | Phones are steered to the standby and reconnect there |
Live state is the easy part; records decide how long the pause lasts. Here is the plan if the region holding Mumbai fails at 8 pm, with Ananya mid-trip:
- 1Declare ithealth checks fail from several places and a person confirms, so a network blip cannot trigger a failover
- 2Steer phonesDNS and the apps' server lists point to the standby; phones reconnect there
- 3Rebuild live statedrivers' next pings refill the geo index within seconds
- 4Promote recordsthe standby's copy becomes the primary; trips from the last few seconds may be missing
- 5Replaydriver apps kept the current trip and queued its events; conditional updates accept each one once
- 6Resume bookingsnew bookings pause for 1–2 minutes; riders see "busy, try again shortly", not an error
Payments dedupe on the trip id, so a capture sent just before the failure is never charged again from the standby; reconcile with the provider when the old region returns. Drill the failover every few months: a recovery plan nobody has tried is a backup nobody has restored.
Part 5 · Going further
Fares, surge and payment
Optional: how Ananya's ₹250 was priced, how surge works, and how payment never charges twice. The fare is worked out at quote time. The routing service from the maps chapter gives the trip's distance and driving time, and the city's rate card turns them into a price:
fare = (base + per_km × distance + per_min × time) × surge
= (₹50 + ₹10 × 10 km + ₹2 × 25 min) × 1.25
= (₹50 + ₹100 + ₹50) × 1.25 = ₹250Example rates only. The quote stores the fare and multiplier, so the rider pays what they saw unless the trip changes.
Surge is a multiplier that rises when riders outnumber free drivers in an area. It trims demand from riders who can wait and draws drivers in from quieter areas. Every minute, a stream job turns each area's recent requests per free driver into a multiplier; an area might be a length-5 geohash cell, about 5 km across. An example curve:
| Requests per free driver | Multiplier |
|---|---|
| 1 or less | 1.0× |
| 1.5 | 1.25× |
| 2 | 1.5× |
| 3 or more | 2.0×, the cap |
Smooth the ratio over several minutes, move in small steps and cap it, so prices do not jump when a stadium empties; some regulators set caps of their own.
Payment completes after the trip, never inside it. Ananya pays by card, so booking placed a hold for ₹250: the money is reserved, not moved, and a cancelled trip releases it. The trip completed event lands on a queue, and a payment worker captures the final fare, or charges methods without holds, with the trip id as the idempotency key. A charge that keeps failing becomes dues, collected before the next booking. A ledger, where every debit has a matching credit, records each move: rider −₹250, driver +₹200, platform +₹50. The payments chapter goes deeper.
The driver must be free the moment a trip ends. If ending a trip waited on the payment provider, a provider outage would leave drivers stuck instead of taking the next ride.
Part 6 · Interview kit
The 45-minute answer
A 45-minute slot leaves about 40 minutes of design once introductions and your own questions are done. Here is ride-sharing on that clock.
| Minutes | What you say and draw | Key number or decision |
|---|---|---|
| 0–5 | Requirements: nearby vehicles, quote, book, track, pay; drivers go online and accept offers. Out of scope: shared and scheduled rides, driver sign-up | Most matches within 15 s; one driver, one trip; 99.99% for booking |
| 5–8 | Only the estimates that decide something: pings against bookings | 250,000 pings/s means memory, not a database; 350 bookings/s suits an ordinary database |
| 8–13 | API: quote, then book with an idempotency key; offers and positions over a WebSocket. Data: live keys that expire, trip records that must never be lost | driver:{id} expires 30 s after the last ping; payments.trip_id is unique |
| 13–22 | Connection servers, location service and geo index, matcher, trip service and trips database; payments and surge to the side | Trips sharded by city; live state can be lost, records cannot |
| 22–37 | Core deep dive: tracking (one overwrite per ping, rare cell changes, partition by city, home-screen snapshots). Then offers with deadlines and conditional accepts | Almost every ping is one in-memory overwrite; offers expire after 15 s |
| 37–40 | Wrap-up: a silent driver, a deploy, a zone or region loss; what you would monitor | Each city homed in one region with a warm standby; bookings pause 1–2 minutes on failover |
Pick the deep dive from the interviewer's question. How do you find drivers fast? Go deep on tracking and the geo index. How does one driver never get two riders? Go deep on offers, deadlines and conditional updates. Retries or money? Walk the trip state machine and payment keyed by the trip id.
- Say
- "Driver locations live in memory, partitioned by city, and expire 30 seconds after the last ping; the next round of pings rebuilds them."
- Say
- "I rank candidates by driving time to the pickup, not straight-line distance: 600 m across a railway can be 12 minutes."
- Say
- "Offers expire after 15 seconds and an accept is a conditional update, so a dead phone costs the rider 15 seconds, never a double booking."
- Avoid
- "Write every ping to the trips database." That is 250,000 writes a second of positions that are stale 4 seconds later.
Interview drill
Interviewers change the problem to see whether your design bends or breaks. Try each one before opening the answer.
New Year's Eve: just after midnight, requests in a few city centres jump to ten times normal within minutes. What do you do?
- Even tenfold bookings everywhere, about 3,500 a second, would suit the trips databases; the strain is a few hot cells and their matching.
- Batch-match in the hot cells, let surge rise within a minute to draw drivers in, and queue riders with an honest wait estimate.
- Before known events, split the hot cells' shards by cell prefix and add connection servers.
- Shed what can wait: refresh home-screen snapshots every 10 s, not 2.5 s.
Add scheduled rides: a rider books tonight for a 5 am airport pickup. What changes in the data and the flow?
- Trips gain a state before requested, scheduled, holding the pickup time; the fare is fixed at match time.
- A task scheduler (the task scheduler chapter) moves each trip to requested about 15 minutes before pickup, and normal matching takes over.
- Few drivers are online at 5 am: start matching earlier, or reserve a driver the night before.
Finance asks you to halve the cost of the real-time tier without hurting matching. Where do you look?
- Connection servers: scale with the hour; at 4 am a fraction of the peak fleet is enough.
- Home screen: snapshots every 5 s instead of 2.5 s, and no pushes while the app is in the background.
- Routing: rank candidates with cached cell-to-cell driving times; compute exact times only for the chosen driver.
- Storage: keep raw pings for a day, and move route traces older than 90 days to an archive tier, 20–50 times cheaper.
- Common miss
- Locking the driver's database row for every offer. Reserve the driver in memory with a compare-and-set; the trip's conditional update settles the accept.
- Common miss
- Charging inside the "end trip" request. A payment provider outage then traps drivers; take payment after the trip, from an event keyed by the trip id.
Wrap-up
Next, Design a Microblog swaps moving cars for posts that must reach millions of followers.
Part 7 · Check yourself
Quick check
Q1Time to match in one city has doubled tonight; bookings are normal, no alarm has fired, and drivers see few offers. What do you check?
Think it through, then check.
- Undelivered offers: a session directory pointing at restarted connection servers sends offers to dead sockets, each waiting out its 15 s.
- Late pings: drivers look silent and are skipped, so searches find fewer candidates.
- Compare offers sent, delivered and answered, and ping lag per shard; then alert on time to match.
Q2Product adds shared rides: two riders going the same way share a car at a lower fare. What changes in matching, the trip model and pricing?
Think it through, then check.
- A car holds seats and an ordered list of stops, so one ride groups several trips.
- Matching asks which car on a trip can take this rider with a detour under, say, 5 minutes for those aboard.
- The compare-and-set reserves a seat, not the whole driver; the fare is discounted and arrival shown as a range.
Q3For a city with 100,000 drivers online, would you run one matcher per city as a leader, or many matchers using compare-and-set? Why?
Think it through, then check.
- One leader: no races and easy batching, but one machine's ceiling and a pause of seconds during failover.
- Many matchers: they scale out and nothing pauses when one dies, but every offer needs a compare-and-set, and batching is harder.
- A middle path: one matcher per area, each holding a lease (the coordination chapter).
Q4At 8 pm the region where Mumbai is homed fails while Ananya is mid-trip. What do she and a rider trying to book see?
Think it through, then check.
- Ananya: the driver app keeps the trip and queues its events; her map may freeze for a minute, then the trip ends normally.
- The new rider: "busy, try again shortly" for 1–2 minutes, while phones move to the standby and its copy of the records is promoted.
- Afterwards, queued events replay through conditional updates, and payments dedupe on the trip id.
Q5Which building block should fire "no driver found after 2 minutes" and "driver silent for 2 minutes", and why not a sleeping thread in the matcher?
Think it through, then check.
- A task scheduler's delayed tasks (the task scheduler chapter): durable timers that survive a matcher crash or a deploy. A sleeping thread dies with its process, leaving the trip waiting forever.
- Timers fire at least once, so each move is conditional: cancel only if the trip is still requested.