Skip to lesson
‹

Courses / System Design Interview / Chapter 27

Module 3 · Design Real Products · Chapter 27 of 39

Design a Ride-Sharing App

20 sections · ~42 min✓ Fact-checked

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 scopeOut of scope today
RiderSee nearby vehicles, get a fare, book, track the driver, pay, rateShared rides, scheduled rides, trips with several stops
DriverGo online, accept offers, navigate, start and end trips, see earningsSign-up, document checks, incentive schemes
PlatformDriver locations, matching, the trip lifecycle, surge pricing, paymentMaps 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:

QuantityWorkingResult
Bookings, peak≈ 116 /s average × 3≈ 350 /s
Fare checks, peakabout 5 per booking≈ 1,750 /s
Location updates, peak1 M drivers ÷ 4 s250,000 /s
Location data in, peak250,000 /s × about 100 B≈ 25 MB/s
Trips in progress, peak350 /s × 1,800 s (30 min with pickup)≈ 630,000
Positions pushed to riders630,000 trips ÷ 4 s≈ 160,000 /s
Open WebSockets, peak1 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.

What the numbers decide

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.

http
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 riders

Positions 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.

Pushes are hints; the server is the truth

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.

ridersid (pk)namephoneratingdefault_payment
driversid (pk)namephonecity_idvehicle_typeplaterating
quotesid (pk)rider_idfaresurgeexpires_at
tripsid (pk)city_idrider_iddriver_idassignment_noquote_idstatusoffered_tooffer_expires_atpickupdropfarerequested_atcompleted_at
trip_eventstrip_id + seq (pk)from_statusto_statusactorat
paymentsid (pk)trip_id (unique)amountmethodstatusprovider_ref
rider_tripsrider_id + completed_at (pk)trip_idcity_idfaresummary

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 keyHoldsLifetime
driver:{id}Position, heading, cell, state (free, offered, on trip), trip id, vehicle, last ping timeExpires 30 s after the last ping
cell:{geohash}:{vehicle}Ids of the free drivers in that length-6 cellChanges when a driver changes cell or state
surge:{area}The current multiplier for the areaReplaced 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.
Animated sceneRide 812: a silent driver and a late accept0:26

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

Core deep dive

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 placesNearby drivers
Items200 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:

What happens to one ping
  1. 1Arrivethe connection server gets a small batch of positions from the driver app
  2. 2Overwriteset driver:{id} to the newest position, expiring in 30 s
  3. 3Check the cellwork out the length-6 geohash; if it is unchanged, the usual case, skip step 4
  4. 4Change cellsotherwise move the driver from the old cell's set to the new one
  5. 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:

Per-cell snapshots for the home screen
  1. 1Subscribethe app subscribes to the cells on its screen over its WebSocket, and changes them as the map moves
  2. 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
  3. 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.

A picture, not a promise

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:

DriverStraight lineDriving time to pickupRank
A1.5 km4 min: on the same main road1
B1.1 km6 min: two signals and a U-turn2
C0.6 km12 min: across the railway, nearest bridge 3 km away3
A

Straight line1.5 km

Driving time to pickup4 min: on the same main road

Rank1

B

Straight line1.1 km

Driving time to pickup6 min: two signals and a U-turn

Rank2

C

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:

RaceWithout a guardThe guard
Two matchers pick B at onceB gets two offersCompare-and-set on driver:B in the in-memory store: free becomes offered only if still free
Ananya cancels as B acceptsB drives to an empty cornerAccept needs the trip still requested; a later cancel is pushed to B at once
B taps Accept twiceTwo assignments, or an errorThe repeat finds B already assigned and gets the same answer
A accepts after the deadlineA takes a ride now offered to BThe 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:

sql
-- 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.

A losing accept must fail cleanly

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.

StateMeaningCan move to
requestedWaiting for a driveraccepted, cancelled
acceptedDriver assigned, heading to the pickuparriving, cancelled; back to requested if the driver drops out
arrivingDriver at or within about 100 m of the pickup; free waiting time runson trip, cancelled
on tripRider in the car, after the driver enters the rider's 4-digit codecompleted
completedRider dropped; payment followsnothing: final
cancelledEnded before the ride began, by a cancel or no driver foundnothing: 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:

Animated sceneRide 812, state by state0:26

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.

Reassigning a silent driver safely
  1. 1Noticethe timer fires: the trip is still accepted, but B has sent no pings for 2 minutes
  2. 2Void B's assignmentone conditional update sets the trip back to requested and clears the driver, only if it is still accepted by B
  3. 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
  4. 4Match againB is left out of this trip's search and stays off the free list until B pings again
  5. 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:

ProblemEffectResponse
A location shard diesThat city's drivers vanish from searchThe replica takes over; even a blank node refills within seconds, since pings carry the trip id, if any
Driver's phone dies mid-tripNo pings, no "end trip"The trip stays on trip; queued actions replay on reconnect; trace gaps are filled from the planned route
A matcher crashesIts requests stallIts leases expire; another matcher restarts the trips still requested
A connection server diesAbout 50,000 phones lose their socketThey reconnect through the load balancer and fetch the current state
A deploy restarts the fleetAll 40 connection servers at once would send 2 M phones reconnecting togetherRestart 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 endsThousands of requests in a few cellsBatch matching, surge, a marked pickup zone; spread the hot cells over more nodes
A zone or a whole region failsA zone: a share of every tier. A region: every city homed thereZone: 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:

ChoiceGainCostFlips when
Pings every 4 s, not 2 sHalf the writes, battery and dataThe car on the map lags up to 4 sRiders watch the last few hundred metres; ping faster near the pickup only
Batch matchingLower total pickup time1–2 s of extra wait per matchQuiet nights, when requests rarely overlap
Offers to one driver at a timeNo lost races; fair to driversA silent driver costs 15 sDrivers are scarce and waits are long: offer two or three at once
Many matchers with compare-and-setScale out; no pause when one diesA guard on every offer; batching is harderBatching matters more: one leader per area of a city
Each city homed in one regionOne writer per trip, no conflictsA region loss pauses bookings 1–2 minutesThose pauses cost more than resolving conflicts between regions
Pings every 4 s, not 2 s

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

Batch matching

GainLower total pickup time

Cost1–2 s of extra wait per match

Flips whenQuiet nights, when requests rarely overlap

Offers to one driver at a time

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

Many matchers with compare-and-set

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

Each city homed in one region

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:

StateIf a zone failsIf the region fails
Live locations, in memoryA replica of each city shard in another zone takes overRebuilt in the standby from drivers' next pings, within seconds
Trips and paymentsA synchronous replica in a second zone: no committed trip is lostAn asynchronous copy in the standby: the last few seconds may be missing
Connection serversSpread over zones; phones reconnect to the restPhones 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:

The region holding Mumbai fails at 8 pm
  1. 1Declare ithealth checks fail from several places and a person confirms, so a network blip cannot trigger a failover
  2. 2Steer phonesDNS and the apps' server lists point to the standby; phones reconnect there
  3. 3Rebuild live statedrivers' next pings refill the geo index within seconds
  4. 4Promote recordsthe standby's copy becomes the primary; trips from the last few seconds may be missing
  5. 5Replaydriver apps kept the current trip and queued its events; conditional updates accept each one once
  6. 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:

text
fare = (base + per_km × distance + per_min × time) × surge
     = (₹50  + ₹10 × 10 km       + ₹2 × 25 min)   × 1.25
     = (₹50  + ₹100              + ₹50)           × 1.25  =  ₹250

Example 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 driverMultiplier
1 or less1.0×
1.51.25×
21.5×
3 or more2.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.

Do not charge inside the trip

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.

MinutesWhat you say and drawKey number or decision
0–5Requirements: nearby vehicles, quote, book, track, pay; drivers go online and accept offers. Out of scope: shared and scheduled rides, driver sign-upMost matches within 15 s; one driver, one trip; 99.99% for booking
5–8Only the estimates that decide something: pings against bookings250,000 pings/s means memory, not a database; 350 bookings/s suits an ordinary database
8–13API: quote, then book with an idempotency key; offers and positions over a WebSocket. Data: live keys that expire, trip records that must never be lostdriver:{id} expires 30 s after the last ping; payments.trip_id is unique
13–22Connection servers, location service and geo index, matcher, trip service and trips database; payments and surge to the sideTrips sharded by city; live state can be lost, records cannot
22–37Core deep dive: tracking (one overwrite per ping, rare cell changes, partition by city, home-screen snapshots). Then offers with deadlines and conditional acceptsAlmost every ping is one in-memory overwrite; offers expire after 15 s
37–40Wrap-up: a silent driver, a deploy, a zone or region loss; what you would monitorEach 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.

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.

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.

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.

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.

Next chapter · 28 of 39Design a Microblog~39 min · 2 animated scenes