Course contents

Design Uber

Design Uber the way a strong candidate builds it on the board: fare quotes, ride requests, matching and accept, one requirement at a time, then deep dives on 400k location writes a second in an in-memory geo index (Redis GEOSEARCH or H3 rings), adaptive pings, a Redis lock plus a conditional commit so a driver never gets two rides, a Kafka queue keyed by zone for stadium spikes, durable workflow timers, push instead of polling, and geo-sharding with Spanner.

FreeMust know for interviewsHardDesign problem37 min readMid → StaffReviewed Oct 2026
On this page
  1. 1. Understand the problem
  2. 2. The set up
  3. 3. High-level design
  4. 4. Deep dives
  5. 5. The final design
  6. 6. What interviewers look for by level
  7. 7. Follow-up questions
  8. 8. How to say it in the interview

🚕 What is a ride-hailing app like Uber?

It's a marketplace that pairs a rider who wants to go somewhere with a nearby driver who is free, then keeps both phones in sync until the trip ends. Think of an old taxi dispatcher with a radio and a city map covered in pins, except the pins move every few seconds for a million cars and the dispatcher has about a minute to find a match.

For this problem, we'll design the core ride flow: quoting a fare, requesting a ride, matching a nearby driver, and getting the driver to the pickup and the drop-off. We'll leave ratings, scheduled rides and payments internals out. Interviewers love Uber because it packs three classic problems into one prompt: a write-heavy stream of moving points, a "who's near me" search, and a booking that must never give one driver two rides.

The 30-second answer

"Riders get a fare quote, then request a ride against that quote. Driver apps send their location every few seconds to a location service that keeps an in-memory geo index (Redis geo or H3 cells), not the main database. A ride request goes onto a queue partitioned by pickup zone; a matcher reads nearby available drivers from the index, ranks them by ETA, and offers the ride to one driver at a time. A Redis lock with a 10 second TTL stops two matchers offering the same driver, and acceptance is one conditional transaction in a strongly consistent store. A durable workflow owns the 10 second offer timers, and updates reach the apps over a persistent push connection instead of polling."

What you’ll learn

  • Build the design one functional requirement at a time: fare, request, match, accept
  • Keep ~400k location writes/s in an in-memory geo index (Redis geo or H3 rings), not the ride database
  • Cut ping load with adaptive intervals decided on the phone, with a server override
  • Stop double offers with a Redis lock (SET NX PX 10000) plus one conditional transaction on accept
  • Survive spikes and crashes with Kafka keyed by zone, commit-after-match and durable workflow timers
  • Replace polling with push over a persistent stream, and geo-shard everything by region

Interview radar

What they probe: where you put hundreds of thousands of location writes a second, how you find nearby drivers without a scan, how you guarantee one driver gets one ride, and what happens at a stadium on a Saturday night. Where candidates slip: writing every GPS ping to Postgres, WHERE distance < 2 km with no spatial index, a lock that lives inside one server's memory, and apps polling every second.

011. Understand the problem

Functional requirements

Here's the scope I'd agree on in the first few minutes. Users should be able to:

  1. Get a fare estimate. A rider enters a pickup and a destination and sees a price and an ETA.
  2. Request a ride from that estimate.
  3. Be matched with a nearby available driver. The system finds one, not the rider.
  4. Accept or decline as a driver, then navigate to the pickup and on to the drop-off.

Below the line

  • Ratings for riders and drivers.
  • Scheduled rides and airport queues.
  • Ride categories like X, XL and Black (they change the filter, not the design).
  • Pooled rides with several riders in one car.
  • Payments internals (we call a payment provider when the trip ends).

Before reading on: what are the non-functional requirements? Think about speed, correctness, spikes and the location stream. Put a number on each.

Non-functional requirements

The system should:

  • Match fast, or fail fast. A rider gets a driver within about 1 minute, or a clear "no drivers available". Nobody waits five minutes for a spinner.
  • Be strongly consistent for matching. A driver is never offered or assigned two rides at the same time, and a ride never gets two drivers.
  • Absorb spikes. When a concert ends, ~100k requests can arrive from a few blocks around one stadium in a few minutes. None of them should be dropped.
  • Ingest locations with low latency. A driver's position should be searchable within a second or two of the ping, at hundreds of thousands of writes a second.

Below the line

  • GDPR and location privacy (retention, deletion, consent).
  • CI/CD and deployment.
  • Monitoring, logging and alerting.
  • Multi-region disaster recovery.
Four non-functional requirements is plenty. Pick the ones that

change the design and attach a number to each, then check in: "I'm going to prioritise consistency for matching and throughput for locations. Does that match what you care about?" That one sentence tells the interviewer where your deep dives will go.

022. The set up

Plan the approach

We'll build the design one functional requirement at a time, the simplest thing that works for each, with a picture at every step. Then the non-functional requirements drive the deep dives: the location stream, the double-booking problem, the stadium spike, timeouts, push and scale. Get something working on the board first, then make it fast and correct.

Core entities

Five entities carry the whole design:

  • Rider: the person who requests rides. Profile and payment method on file.
  • Driver: the person who drives. Car details and a status: OFFLINE, AVAILABLE, OFFERED or ON_RIDE.
  • Fare: a priced quote for one pickup and destination, with an expiry. The rider requests a ride from a fare.
  • Ride: one trip from request to drop-off. Holds the fare, the assigned driver and a status that moves REQUESTED → ACCEPTED → PICKED_UP → COMPLETED (or CANCELLED).
  • Location: a driver's latest latitude, longitude and timestamp. Tiny, hot and rewritten every few seconds.
Fig. 1

Five entities carry the whole design

Five entities carry the whole designFive boxes. A Rider asks for a Fare (a priced quote with an expiry). A Fare becomes exactly one Ride when the rider confirms it. A Ride is requested by a Rider and assigned to at most one Driver. A Driver reports a Location every few seconds. Notes at the bottom list the Ride and Driver status values.Riderid, name, ratingFareprice, route, expiresAtRidestatus, fareId, driverIdDriverstatus, car, ratingLocationlat, lng, ts (latest)asks forbecomes (1:1)requestsassigned toreports every 5 sRide.status: REQUESTED → ACCEPTED → PICKED_UP → COMPLETED (or CANCELLED)Driver.status: OFFLINE · AVAILABLE · OFFERED · ON_RIDE
The entity map. A Fare becomes exactly one Ride, a Ride has at most one Driver, and each Driver reports one latest Location. The two status lines at the bottom are the state machines the rest of the design protects.

API

One endpoint per functional requirement. Here's how I'd write them:

POST  /fares               { pickup, destination }   -> Fare { fareId, price, eta, expiresAt }
POST  /rides               { fareId }                -> Ride { rideId, status: "REQUESTED" }
POST  /drivers/location    { lat, lng }              -> 204 No Content
PATCH /rides/:rideId       { action: "accept" | "decline" }   -> Ride | 409 Conflict

Why these verbs? POST /fares creates a quote resource, which is why it's a POST and not a GET: the server stores it and gives it an ID. POST /rides creates the ride from that ID. Location is a fire-and-forget write, so 204 keeps the response empty. PATCH changes one field of an existing ride, and it can return 409 because another driver, or a cancel, may have got there first.

Never trust the client with identity, time or money. The rider and driver IDs come from the

JWT, not the body, so a driver can't accept on someone else's behalf. Timestamps are set by the server (a phone's clock can be minutes off). And the price is never sent by the client: the ride references a fareId the server created and stored, so nobody can request a ride for $1.

Back-of-envelope estimates

Only the numbers that change the design. All of these are assumptions to confirm, and the arithmetic is ours.

Quantity Assumption Roughly
Drivers online at peak ~10M registered, 1-2M online 2M to be safe
Location writes 2M drivers, one ping every 5 s ~400k writes/s
Ping size id, lat, lng, timestamp, status ~100 bytes, so ~40 MB/s in
Live location state 2M x ~100 B ~200 MB, fits in memory
Ride requests ~20M rides/day, peaks at 3x ~230/s average, ~700/s peak (more at one stadium)

Here's the catch these numbers show. Locations are a huge write rate on tiny state; rides are a low rate that must be exactly right. Those two want different storage, and most of this design follows from keeping them apart.

033. High-level design

Let's satisfy the functional requirements one at a time. Each picture is the previous one plus a few new boxes, drawn in colour; what we already had turns gray.

1) Riders should be able to get a fare estimate

The rider types a pickup and a destination and taps "See prices". We need three things:

  • Rider app: the client. It talks to us over HTTPS.
  • API gateway: one front door that checks the JWT, applies rate limits and routes each call to the right service.
  • Ride service: owns fares and rides. For a quote, it needs a route.
  • Maps API: a third-party routing provider (Google Maps, Mapbox or an in-house engine) that returns distance and ETA.
  • Ride DB: a relational database. For now it holds one table, Fare.
Fig. 2

Step 1: the rider gets a fare estimate

Step 1: the rider gets a fare estimateThe board after the first requirement. The rider app calls the API gateway with POST /fares (step 1). The gateway routes it to the Ride service (step 2). The Ride service asks a third-party maps API for the route, distance and ETA (step 3), prices it, and saves a Fare row with an expiry in the Ride DB (step 4) before returning the fare to the rider.RiderAPI Gatewayauth · routingRide Servicefares · ridesMaps APIroute · ETARide DBTables- Fare12route + ETA3save Fare4POST /fares{ pickup, destination }→ { fareId, price, eta, expiresAt }
The first board. Four boxes and a third party are enough to quote a price. Follow badges 1 to 4.
  1. The rider app sends POST /fares with the pickup and destination to the API gateway.
  2. The gateway authenticates the rider and routes the call to the Ride service.
  3. The Ride service asks the Maps API for the route, distance and ETA.
  4. It prices the trip (base + per km + per minute, times any surge), saves a Fare row with an expiresAt a few minutes out, and returns it to the rider.

Saving the fare is the important bit. It's what lets the next step trust the price.

2) Riders should be able to request a ride from that estimate

The rider taps "Confirm". We don't add a single service here: the Ride service already owns this. We only add a Ride table.

Fig. 3

Step 2: the rider requests a ride from that fare

Step 2: the rider requests a ride from that fareSame board, no new services. The rider app sends POST /rides with the fareId (step 1); the gateway routes it to the Ride service (step 2). The Ride service loads the Fare, checks it belongs to this rider and has not expired (step 3), then inserts a Ride row with status REQUESTED (step 4). The only new thing is the Ride table in the Ride DB.RiderAPI Gatewayauth · routingRide Servicefares · ridesMaps APIroute · ETARide DBTables- Fare- Ride12load Fare3insert Ride4POST /rides{ fareId }→ { rideId, status: REQUESTED }
Same board, one new table. The request carries only a fareId, so the price comes from our own database, not from the phone.
  1. The rider app sends POST /rides with { fareId }.
  2. The gateway routes it to the Ride service, with the rider ID taken from the JWT.
  3. The Ride service loads the Fare and checks it belongs to this rider and hasn't expired.
  4. It inserts a Ride row with status = REQUESTED and the fare's price, and returns the rideId.

Use the fareId as an idempotency key as well: if the phone retries on a flaky network, the second POST /rides finds the existing ride instead of creating a second one.

3) The system should match the ride with a nearby available driver

Now the interesting part. To find a driver near the pickup, we need to know where drivers are. We add four things:

  • Driver app: sends its location every few seconds while online.
  • Location service: receives those pings and keeps the latest position per driver.
  • Location store: where the latest positions live, queryable by "who's near this point?". Deep dive 1 decides what this really is.
  • Matching service: given a ride, finds the best available driver near the pickup.
Fig. 4

Step 3: match the ride with a nearby driver

Step 3: match the ride with a nearby driverNew parts in colour: the driver app, a Location service, a location store and a Matching service. Step 1: every driver app sends its position through the gateway to the Location service every few seconds. Step 2: the Location service upserts it into the location store. Step 3: the Ride service hands the new ride to Matching. Step 4: Matching asks the location store for available drivers near the pickup and picks the best one.RiderAPI Gatewayauth · routingRide Servicefares · ridesMaps APIroute · ETARide DBTables- Fare- RideDriverMatchingfind a driverLocation Svcdriver pingsLocation store1upsert2match(ride)3nearby?4
The board grows a second half. The bottom is a write-heavy location pipeline; the top is the low-rate ride path. Matching is where the two meet.
  1. Every few seconds, each driver app sends POST /drivers/location through the gateway to the Location service.
  2. The Location service upserts the driver's latest position into the location store.
  3. After the ride is created, the Ride service hands the rideId to the Matching service.
  4. Matching asks the location store for available drivers within a few km of the pickup, ranks them (closest ETA first), and picks the best one.

4) Drivers should be able to accept or decline, then navigate

Matching has a driver in mind; now it has to ask her. We add one service:

  • Notification service: sends push messages to phones through APNs (iOS) and FCM (Android).
Fig. 5

Step 4: offer, accept, and keep both apps updated

Step 4: offer, accept, and keep both apps updatedThe Notification service is new. Step 1: Matching sends the offer to the Notification service. Step 2: it pushes the offer to the driver app through APNs or FCM. Step 3: the driver accepts with PATCH /rides/:id through the gateway. Step 4: the Ride service marks the ride ACCEPTED in the Ride DB. Step 5: it tells the Notification service. Step 6: the rider gets a "driver on the way" push. The driver then navigates with the maps SDK while location pings continue.RiderAPI Gatewayauth · routingRide Servicefares · ridesMaps APIroute · ETARide DBTables- Fare- RideDriverMatchingfind a driverLocation Svcdriver pingsLocation storeNotificationAPNs / FCMoffer1push offer23acceptACCEPTED45driver on the way6
The complete first pass. The offer goes out by push, the answer comes back as a normal API call, and the rider hears about it by push too. Badges 1 to 6 are one accepted offer.
  1. Matching sends an offer for the ride to the Notification service.
  2. The Notification service pushes it to the driver's phone through APNs or FCM.
  3. The driver taps Accept: PATCH /rides/:id { action: "accept" } goes through the gateway to the Ride service.
  4. The Ride service marks the ride ACCEPTED with this driver, and the driver ON_RIDE.
  5. It tells the Notification service the ride has a driver.
  6. The rider gets a "driver on the way" push.

From here the driver app shows turn-by-turn directions with the maps SDK, first to the pickup and then to the destination, and keeps sending location pings. The driver taps "Picked up" and "Dropped off", which are two more PATCH calls that move the ride's status. If the driver declines, or says nothing, Matching tries the next driver; deep dive 5 makes that robust.

Real-time updates

Any time the server has news the client didn't ask for (an offer, a driver's position, "your food is here"), you have a real-time update problem. The ladder is always the same: polling, then mobile push (APNs/FCM) for the app in the background, then a persistent connection (SSE, WebSocket or gRPC stream) for the app on screen. Deep dive 6 climbs it for Uber; the same moves show up in chat, food delivery and live scores.

This design works for a demo. Now let's break it with the non-functional requirements.

044. Deep dives

Each deep dive answers one non-functional requirement. Deep dives 1 and 2 are about location ingest, 3 is about consistency, 4 and 5 are about spikes and failures, 6 is about latency to the phone, and 7 is about scale. A senior candidate drives two or three of these unprompted and gets them right. A staff candidate also sees how they interact: the same region key shards the geo index, the queue and the ride store.

1) How do we handle frequent driver location updates and find nearby drivers fast?

Our estimate says ~400k location writes a second. Every matching request then asks "which available drivers are within 2 km of this point?". Both have to be fast, and they fight each other if they live in the wrong place.

Before reading on: you have 2M drivers, each with a latitude and longitude that changes every 5 s. Where do you store them, and what does one &quot;nearby&quot; query cost?

Naive approach: Write every ping to the main database

The Location service runs UPDATE drivers SET lat = ?, lng = ? WHERE id = ? for every ping, and Matching runs SELECT ... WHERE lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?. It's what you'd do for a few hundred drivers, and it reuses the database you already have.

Challenges: ~400k row updates a second is far beyond one Postgres primary, and each update churns indexes and the write-ahead log. Worse, a B-tree is one-dimensional. An index on (lat, lng) narrows latitude to a band, then scans every driver in that band across the whole city's width to filter longitude. And all of this shares a database with the ride bookings that must stay fast and correct.

Solid approach: Batch the writes and use a spatial index

Buffer pings in the Location service and write them in batches every second or two, into a database with a real 2D index: PostGIS with a GiST index (an R-tree), or a quadtree you maintain yourself. A quadtree splits the map into four squares, and splits any square with too many drivers again, so dense downtown gets small cells and the suburbs get big ones. "Nearby" becomes "walk down to my cell, check it and its neighbours".

Challenges: batching trades away freshness: a 2 s batch means Matching can see a driver who moved 30 m. And the index still lives on disk and is still rebuilt as drivers move; a quadtree in particular has to split and merge cells as density changes through the day. It's a fine answer at 50k writes a second; at 400k it's a lot of disk work for data that is stale after 5 seconds anyway.

Staff approach: An in-memory geo index, keyed by cell

Locations are tiny, hot and short-lived, so keep them in memory. Two ways to do it, both good:

Redis geo sets. Redis encodes each point as a 52-bit geohash and stores it in a sorted set, so updates and radius searches are both O(log N) plus the results:

GEOADD     drivers:nyc  -73.9857 40.7484  driver:42
GEOSEARCH  drivers:nyc  FROMLONLAT -73.9851 40.7488  BYRADIUS 2 km  ASC  COUNT 20

H3 cells. Convert each ping to an H3 hexagon ID (resolution 9 is about 0.1 km² per cell), keep cell -> set of driver IDs plus driver -> (cell, lat, lng, ts), and only move the driver between sets when the cell changes. A search reads the rider's cell, then ring 1, then ring 2, until there are enough candidates. That's what Uber built H3 for.

Either way, shard by region (one key or cluster per city), keep available drivers in their own set so Matching never filters out busy ones, and drop stale drivers: Redis can't expire one member of a set, so keep a second sorted set lastseen scored by timestamp and remove anyone silent for 30 s, or bucket keys by minute and let old buckets expire.

Challenges: memory isn't durable. If a Redis node dies, its drivers vanish from search until their next ping, about 5 s later. That's acceptable for data that's stale in 5 s anyway: run replicas, and treat the index as rebuildable, never as the source of truth for rides.

Fig. 6

Where do 400k pings a second go?

Where do 400k pings a second go?Two panels. Left, in red: every driver ping becomes an UPDATE on the main Postgres table, about 400 thousand a second, and a nearby query can only use a B-tree on latitude or longitude, so it scans a whole band of the map. Right, in green: pings go to an in-memory geo index in Redis, one per region, written with GEOADD and queried with GEOSEARCH by radius, with stale drivers dropped after about 30 seconds.Naive: every ping to the main DBdriversLocation SvcRides DBPostgres~400k UPDATEs/s on one tableB-tree on lat OR lng, not bothnearby = scan a whole lat bandStaff: in-memory geo indexdriversLocation SvcRedis geoone per regionGEOADD drivers:nyc lng lat d42GEOSEARCH ... BYRADIUS 2 km ASCdrop pings older than 30 s
Same pings, different home. On the left, 400k updates a second hammer the booking database and every search scans a band. On the right, an in-memory index per region answers a radius query in a few milliseconds and forgets drivers who go quiet.

Here's what the H3 version of the search looks like. The rider's cell first, then the six around it, then the twelve around those.

Fig. 7

Find nearby drivers: look in the rider's cell, then the ring around it

Find nearby drivers: look in the rider's cell, then the ring around itA patch of hexagonal cells. The rider sits in the centre cell. The first search looks only in that cell. If there are too few available drivers, the search adds the six cells touching it (ring 1), then the twelve cells around those (ring 2). Each driver is stored under the ID of the cell they are in, so each step is a handful of key lookups. Notes on the right explain why hexagons and how resolutions nest.ring 2Why hexagons?- every neighbour is the same distance away (squares have two distances)- a moving car drifts across cells more evenlyHow it nests- 16 resolutions- each finer cell is ~1/7 the area- the cell ID is a plain 64-bit keyThe searchring 0 → ring 1 → ring 2stop once you have ~10 available driversrideravailable driver
Ring search. Each step is a handful of key reads, not a scan. Stop as soon as you have about 10 available drivers, or at a maximum radius.
Uber built

H3, a hexagonal grid of the whole planet with 16 resolutions, where each finer cell is about one seventh the area of its parent (Uber). Hexagons were chosen because all six neighbours are the same distance from the centre; a square has edge neighbours and corner neighbours at two different distances. That makes "everything within k rings" a fair stand-in for a circle, and Uber uses the same cells to measure supply and demand for surge pricing.

Many candidates say &quot;use a geospatial index&quot; and stop. Name it (

GEOSEARCH, a quadtree, H3 rings), say where it lives (memory, sharded by region), and say what happens to a driver who stops pinging. The stale-driver answer is what separates a memorised answer from a designed one.

2) How do we cut the ping load without losing accuracy?

A fixed 5 s interval for every online driver is simple, but most of those pings say nothing new. A driver parked outside a café for 20 minutes sends 240 identical pings. Meanwhile, a driver 300 m from a pickup really does need to be precise.

Before reading on: which drivers need frequent updates, and which don't? Who should decide the interval: the phone or the server?

Naive approach: Ping faster for better accuracy

"Accuracy matters, so every driver pings every second." The map looks great in testing.

Challenges: 2M drivers at 1 s is 2M writes a second, five times our estimate, plus battery drain and mobile data the drivers pay for. Most of those writes are parked cars.

Solid approach: Drop the boring pings on the server

Keep a fixed interval on the phone, but have the Location service skip the index write when the driver moved less than ~20 m since the last stored point.

Challenges: it saves index writes but not the network calls, TLS work and battery: every ping still crosses the internet and hits the gateway. The expensive part is the request, not the write.

Staff approach: Adaptive intervals decided on the phone

The phone knows things the server doesn't: its speed, whether it's moving, its own status. So the app picks the interval:

  • Offline: no pings at all.
  • Available and parked: every ~30 s, a heartbeat that says "still here".
  • Available and driving: every ~5 s.
  • Heading to a pickup, under 1 km away: every ~2 s, because the rider is watching that car.
  • On a trip: every ~4 s, enough to draw the route and compute the fare.

The server stays in charge by returning a suggested interval in each response (for example, faster in an area with a surge of requests). On the rider's side, the app animates the car smoothly between points, so 4 s updates still look continuous.

Challenges: a slower interval means a parked driver's position can be 30 s old when they pull away. Mitigate it by sending an immediate ping on any state change (moving after parked, going online, accepting) and by checking ts at match time. Old app versions that ignore the hint need a server-side throttle as a backstop.

Fig. 8

The same 30 seconds, four drivers, four ping rates

The same 30 seconds, four drivers, four ping ratesA 30 second timeline with one row per driver state. A parked available driver pings every 30 s (2 marks). A driving available driver pings every 5 s (7 marks). A driver heading to a pickup pings every 2 s (16 marks). A driver on a trip pings every 4 s (8 marks). Notes: the client picks the interval from speed, status and distance to pickup, and the server can override it. With half the fleet parked, total writes drop by roughly 40 percent compared with a fixed 5 s.Parked, availableevery 30 sDriving, availableevery 5 sHeading to pickupevery 2 sOn a tripevery 4 s0 s30 sFixed 5 s: every driver sends 6 pings per 30 s.Adaptive: parked drivers send 1, so with half the fleet idle the total drops ~40%.Who decides?- the app: speed, status, distance to pickup- server can override it
Thirty seconds, four rates. The driver who matters most right now (heading to a pickup) sends the most pings; the parked one sends almost none. With half the fleet idle, total writes drop by about 40% against a flat 5 s.
Many candidates treat the location rate as a fixed input. It isn't: you control it. Saying &quot;the client adapts its interval, and the server can override it&quot; turns a scaling problem into a product decision, and that's a staff-level move.

3) How do we make sure a driver is never offered two rides at once?

Two riders 200 m apart request at the same moment. Two matcher instances each run the nearby search, and the same driver is the best candidate for both. Without coordination, she gets two offers, accepts one, and the other rider waits 10 seconds for nothing. Or worse, both accepts are recorded.

Before reading on: what stops two matcher instances from offering the same driver? Where does that check have to live, given there are dozens of matcher instances?

Naive approach: A lock inside the matching process

Keep a Set<driverId> of drivers with an open offer in the matcher's memory, guarded by a mutex. Check it before offering, remove the driver when she answers.

Challenges: it only works with one matcher instance. With 50 instances, each has its own set, so two of them happily offer the same driver. And if the instance restarts, every in-flight offer is forgotten.

Solid approach: A status column in the database, and an in-memory timer

Make the database the arbiter: a conditional update claims the driver, and only one caller can win.

UPDATE drivers SET status = 'OFFERED', offered_ride = :rideId
 WHERE id = :driverId AND status = 'AVAILABLE';   -- 1 row: you own her. 0 rows: next driver.

The matcher then starts a 10 s timer in memory; on timeout or decline it sets the driver back to AVAILABLE.

Challenges: if the matcher crashes during those 10 s, nothing resets the driver. She sits in OFFERED and gets no rides until a sweeper job notices. It also puts a write per offer on the ride database, which is fine at ~700 rides/s but adds up with several offers per ride at peak.

Staff approach: A distributed lock with a TTL, plus a conditional commit

Claim the driver in Redis with a key that expires on its own after the offer window:

SET lock:driver:42 ride:9 NX PX 10000    -> OK   (we own the offer for 10 s)
SET lock:driver:42 ride:7 NX PX 10000    -> nil  (someone else does: try the next driver)

NX means only set it if it doesn't exist, and PX 10000 is the 10 s offer window. If the matcher dies, the key expires and the driver is offerable again with no sweeper. Release it with a small Lua script that deletes the key only if the value is still your ride ID, so a slow matcher can't delete someone else's lock. Matching also skips locked drivers when it reads candidates.

The lock is about experience: one offer per driver. The guarantee is the commit on accept, one transaction in the strongly consistent ride store:

BEGIN;
UPDATE rides   SET status = 'ACCEPTED', driver_id = :d WHERE id = :r AND status = 'REQUESTED';
UPDATE drivers SET status = 'ON_RIDE',  ride_id   = :r WHERE id = :d AND status IN ('AVAILABLE', 'OFFERED');
-- either update touched 0 rows: ROLLBACK, return 409
COMMIT;

Challenges: a Redis lock isn't perfectly safe: a failover can lose a key that was just written, and a long GC pause can outlive the TTL. That's why it's not the only line of defence. The transaction catches anything the lock misses, and the worst case is a rare 409 that the driver app turns into "this ride was taken".

Fig. 9

Two matchers, one driver: the lock decides

Two matchers, one driver: the lock decidesSequence diagram with four participants: Matching A, Redis, Matching B and the driver. Step 1: A runs SET drv42 A NX PX 10000 and gets OK. Step 2: B runs the same command for rideB and gets nil because the key exists, so B moves on to its next candidate. Step 3: A sends the offer to the driver with a 10 second window. Step 4: the driver accepts. Step 5: A commits the ride with a conditional update and deletes the lock. If the driver never answers, the key expires on its own after 10 seconds.1SET drv42 A NX PX 10000OK: A owns the driver2SET drv42 B NX PX 10000nil: already held3offer rideA, 10 s to answer4accept5commit ride, then DEL lockMatching ARedisMatching BDriver 42No answer in 10 s? The key expires by itselfPX 10000 = the offer window, so a crashed matcher never strands a driverB tries its next driver
The lock decides. Matching A wins the key (1), Matching B gets nil and moves on (2), A offers (3), the driver accepts (4), and A commits the ride and releases the lock (5). If the driver never answers, the 10 s TTL cleans up for free.

And here's the whole request-to-accept path that the lock sits in:

Fig. 10

Request to accept: one driver, one ride, no double booking

Request to accept: one driver, one ride, no double bookingNumbered steps. 1: the rider requests a ride and the Ride service creates it as REQUESTED. 2: Matching reads available drivers from the rider's cells. 3: it ranks them by ETA. 4: it locks the best driver and sends the offer with a 10 second window. 5: the driver accepts. 6: the Ride service runs one conditional transaction that checks the ride is still REQUESTED and the driver still free, then updates both. If the driver declines or times out, the next driver gets the offer.riderRide Serviceowns ride stateMatchingGeo indexcell → driversETA servicedriverRide DBone transactionrequest1find drivercells2rank3lock + offer, 10 s4accept56IF ride = REQUESTED AND driver = OFFERED to meTHEN ride = ACCEPTED, driver = ON_RIDEdecline ortimeout?next driver
From request to accept. The lock (4) keeps the offer exclusive; the transaction (6) makes the assignment correct. A decline or a timeout sends the offer to the next driver.
  1. The rider requests; the Ride service creates the ride as REQUESTED.
  2. Matching reads available drivers from the rider's cells.
  3. It ranks them by ETA on the road network (a driver 300 m away across a river can be 10 minutes out).
  4. It locks the best driver for 10 s and sends the offer.
  5. The driver accepts.
  6. The Ride service runs the conditional transaction: ride still REQUESTED, driver still free. Both change, or neither does.
Many candidates update the ride in one service and the driver's status in another, one call after the other. If the second call fails, you get a driver marked busy with no ride, or a ride whose driver is taking other trips. Keep both rows in

one transactional store, or be ready to explain a Saga with compensating steps.

4) How do we make sure no ride request is dropped at peak, or when a matcher crashes?

The concert ends and 100k requests arrive in a few minutes from the cells around one stadium. In our current design, the Ride service calls Matching directly. Matching instances fill up, calls time out, and any request that was in a crashed instance's memory is simply gone. The rider sees a spinner, then an error.

Before reading on: how do you take a burst 100x bigger than normal without losing a request, and without making every matcher handle every region?

Naive approach: Match inline in the request

POST /rides calls Matching synchronously and holds the HTTP request open until a driver accepts, up to a minute.

Challenges: a minute-long request ties up a connection and a thread per rider, so 100k riders means 100k open calls through the gateway. A deploy or crash anywhere in the chain loses the request, and the client's retry creates confusion about which attempt is real.

Solid approach: An in-memory work queue in front of Matching

POST /rides returns right away with REQUESTED; the Ride service puts the ride ID on a queue that a pool of matcher workers drains.

Challenges: better, but if that queue is in a process's memory, a crash still drops whatever was queued. A single queue also mixes all regions together, so a stadium in one city slows matching for everyone else.

Staff approach: A durable log partitioned by zone, committed after the match

Write each ride request to Kafka, keyed by pickup zone (a coarse H3 cell or a city district), so every partition holds one area's requests in order. Matchers form a consumer group: each partition is read by exactly one matcher at a time, which also keeps that matcher's view of local drivers warm.

The rule that makes it lossless: commit the offset only after the ride is matched or definitively failed. If a matcher crashes mid-ride, Kafka rebalances its partitions to another matcher, which resumes from the last committed offset. Matching is idempotent per rideId, so a replayed request that was already matched is a no-op.

Autoscale matchers on consumer lag. A stadium spike shows up as lag in one zone's partitions, not as dropped calls, and every request carries its deadline: if it's still unmatched at 60 s, fail it and tell the rider.

Challenges: one partition is consumed by one matcher, so a single hot partition caps that area's throughput. Mitigate it with small zones (many partitions per city) so a stadium spreads over several, and keep enough partitions up front, because adding them later reshuffles keys.

Fig. 11

Ride requests wait in a log, not in a matcher's memory

Ride requests wait in a log, not in a matcher's memoryThe Ride service writes each ride request to a Kafka topic partitioned by pickup zone; three partitions are shown. Each partition is read by one matcher in a consumer group. Matcher 2 has crashed (red cross); its partition is rebalanced to Matcher 3, which resumes from the last committed offset, so nothing is lost. Notes: commit the offset only after the ride is matched or failed, and autoscale matchers on consumer lag. A stadium spike of 100k requests piles up as lag in the log instead of being dropped.Ride Service...p0 · zone A...p1 · zone B (stadium)...p2 · zone CMatcher 1Matcher 2Matcher 3rebalancedCommit offsetonly after theride is matchedor failedAutoscaleon consumer lag,up to one matcherper partition100k requests after a concert pile up as lag in zone B, not as dropped calls.Many small zones per city keep one hot partition from becoming the bottleneck.
Requests wait in a log, not in RAM. Matcher 2 crashed, so its partition was rebalanced to Matcher 3, which resumes from the last committed offset. The stadium zone builds lag instead of dropping requests.
Many candidates say &quot;add a queue&quot; and stop there. The interviewer's next question is always &quot;when do you acknowledge the message?&quot;. If you commit on read, a crash loses the ride. Commit after the outcome, and make the handler idempotent.

5) What happens if a driver doesn't respond to an offer?

Drivers miss offers all the time: phone in a cup holder, bad signal, just ignoring it. Each offer needs a 10 s timer, and when it fires we move to the next candidate. At ~700 rides a second, with a few offers per ride, that's thousands of live timers that must survive a matcher crash.

Before reading on: where does the 10 s timer live? What happens to it if the server holding it restarts at second 7?

Naive approach: A sleep or timer inside the matcher

offer(driver); wait up to 10 s for a reply; else next driver, with the timer in the matcher's memory.

Challenges: the timer dies with the process. After a deploy or a crash, rides sit in REQUESTED forever with nobody working on them.

Solid approach: A delay-queue message per offer

When you send an offer, also enqueue a message "check ride r9, driver 42" with a 10 s delay (SQS delay queues, a Redis sorted set scored by due time, or a delayed Kafka topic). When it's delivered, a worker checks: if the ride is still waiting on driver 42, release the lock and offer the next driver.

Challenges: it survives crashes, but the logic is now scattered across an offer handler, an accept handler and a timeout handler that race each other, with state stitched together in the database. Every new rule ("after 3 declines, widen the radius") means changing all three.

Staff approach: A durable workflow per ride

Write the offer loop as plain code and run it on a durable workflow engine like Cadence or Temporal:

for driver in rankedCandidates(ride):
    if not lock(driver, ttl = 10s): continue
    sendOffer(driver, ride)
    reply = await signal("driver_reply", timeout = 10s)   # durable timer
    if reply == ACCEPT and commitRide(ride, driver): return MATCHED
return NO_DRIVER

The engine saves every step to a history store and keeps the timers itself. If the worker running this loop dies at second 7, another worker replays the history and the timer still fires at second 10. The accept from the driver app arrives as a signal to that ride's workflow, so offer, timeout and accept are handled in one place, in order.

Challenges: you operate another system (a Cadence or Temporal cluster with its own database), and workflow code has rules: it must be deterministic so replay gives the same decisions. For a ride, that's a good trade; this is exactly the kind of long-running, timer-heavy flow these engines exist for.

Fig. 12

The offer loop as a durable workflow

The offer loop as a durable workflowTop row: Requested leads to Offer next driver (lock and push), then a 10 second wait. Accept leads to Accepted. Timeout or decline loops back to Offer next driver. After about a minute or too many candidates the ride fails with no driver found. Bottom zone: the workflow engine (Cadence or Temporal) stores every step in a history database, keeps durable timers, and runs the code on stateless workers. If a worker dies mid-wait, another worker replays the history and the timer still fires.RequestedOffer nextlock + pushWait 10 sdurable timerAcceptedNo driveraccepttimeout / decline60 s or N triesWorkflow engine (Cadence / Temporal)History...TimersWorkersstateless · x Neach step is saved firstworker dies → another onereplays history, resumes
The offer loop as a state machine. Timeout and decline loop back to the next driver; after about a minute or too many tries the ride fails cleanly. Underneath, the engine stores history and timers so any worker can pick the loop up.
Fig. 13

One ride, two drivers, one timer

One ride, two drivers, one timerSequence diagram with four participants: the offer workflow, Driver 1, Driver 2 and the Ride DB. Step 1: the workflow offers ride r9 to Driver 1 and starts a 10 second timer. Step 2: the timer fires with no reply. Step 3: the workflow offers r9 to Driver 2. Step 4: Driver 2 accepts after 4 seconds. Step 5: the workflow runs a conditional update on the Ride DB, which reports one row changed. A note says that if the worker crashes between steps, another worker replays the history and the timer still fires.1offer r9 (lock 10 s)no reply: timer fires23offer r9 (lock 10 s)4accept after 4 s5UPDATE rides ... WHERE status = REQUESTED1 row: ACCEPTEDOffer workflowDriver 1Driver 2Ride DBWorker crashes between 1 and 2?another worker replays the history; the timer is stored, so it still firestimer: 10 s
One ride, two drivers. Driver 1 never answers, so the stored timer fires (2) and the workflow offers Driver 2 (3), who accepts (4). The conditional update (5) is the same transaction as in deep dive 3.
Uber built

Cadence for exactly this kind of long-running, failure-proof business flow and open-sourced it (Cadence). Its original authors later forked it as Temporal. Saying "a durable workflow, like Cadence or Temporal" is a strong, concrete answer here.

6) How do we get updates to the rider and driver apps quickly?

During a trip, the rider's map shows the car moving. The driver's phone must ring with an offer within a second. Our design so far uses APNs and FCM for everything.

Before reading on: how does the rider's phone learn the driver moved 40 m? What happens with a million active trips if every phone asks once a second?

Naive approach: Polling

Each app asks "anything new?" every second or two.

Challenges: 2M open apps polling every 2 s is ~1M requests a second, and almost every answer is "no". It drains batteries and still adds up to 2 s of delay. At one point 80% of the requests to Uber's API gateway were polling calls (Uber).

Solid approach: Mobile push for everything

Use APNs and FCM for offers and status changes, as in the high-level design.

Challenges: they're best-effort: no ordering, no delivery guarantee, delays of seconds under load, and small payload limits (4 KB on APNs). Fine for "your driver has arrived" when the app is in the background; too slow and too lossy for a moving car or a 10 s offer.

Staff approach: A persistent connection through a push gateway

While the app is open, it holds one long-lived stream to a push gateway (SSE, WebSocket or a gRPC stream). A trigger service listens to ride and location events and decides when something is worth pushing and who cares; the gateway writes it down the right connection. A small registry maps userId -> gateway node so any service can reach any phone. Every message has a sequence number; on reconnect the app sends the last one it saw and the gateway resends the rest. When the app is backgrounded, fall back to APNs/FCM.

For the moving car, don't forward every ping. Push the driver's position to the rider every 2 to 4 s and animate in between.

Challenges: millions of open connections are stateful, so deploys must drain them slowly and clients must reconnect with jitter, or a gateway restart becomes a thundering herd. Rough math (ours): at ~100k connections per node, 2M phones need about 20 nodes plus headroom.

Fig. 14

Polling asks "anything new?"; push answers only when there is

Polling asks "anything new?"; push answers only when there isLeft: an app polls the backend every few seconds and most answers are "nothing changed". At one point 80% of requests to Uber's API gateway were polls. Right: the app holds one long-lived stream to a push gateway; a trigger service decides when something is worth sending, the API builds the payload, and the gateway writes it down the open stream (SSE first, gRPC streaming later), with sequence numbers and acks.PollingappAPI"anything new?" x N80% of gateway calls were pollsbattery + bandwidth wastedPushappPush gatewayTriggerwhen?APIwhat?one streambuild payloadsendserver speaks only on a changeseq numbers + acks, resend on reconnect
Stop asking, start telling. Polling sends a request for every possible change; push keeps one connection open and the server speaks only when there's news.
Uber built

RAMEN (Realtime Asynchronous MEssaging Network) to replace polling. A service called Fireball decides when to push, the API layer builds the payload, and the gateway delivers it. The first version used Server-Sent Events with sequence numbers for at-least-once delivery; later Uber moved to gRPC bidirectional streaming so acks travel on the same stream (Uber).

7) How do we scale to many cities and keep rides consistent?

One Redis cluster, one Kafka cluster and one ride database work for a country. Globally, a rider in Mumbai shouldn't wait on a round trip to Virginia, and one region's outage shouldn't stop rides everywhere.

Before reading on: what's the natural shard key for Uber? What happens to a rider standing right on the line between two shards?

Naive approach: Scale up and add read replicas

A bigger ride database with read replicas, one big Redis cluster.

Challenges: replicas help reads, but our hard part is writes: locations and accepts. Replica lag also means a matcher could read a driver as free who was assigned 100 ms ago.

Solid approach: Shard the geo index and queue by region

Give each city or region its own location index, Kafka partitions and matchers. Requests are routed by the pickup's cell.

Challenges: the ride database is still one global bottleneck, and cross-region consistency for rides (a rider travelling, a driver near a border) is left to chance.

Staff approach: Geo-sharding end to end, with a strongly consistent ride store

Use one key, the region of the pickup cell, for everything on the hot path: a geo router sends the request to that region's stack, where the geo index, the queue partitions, the matchers and the workflows all live. Almost every ride starts and is matched inside one region, so almost nothing crosses. Only when a search ring crosses a shard line does Matching scatter-gather: query both neighbours, merge, rank.

For the rides themselves, use a database that gives transactions across rows and regions (Spanner, CockroachDB), so the accept transaction from deep dive 3 stays correct no matter which region a call lands in.

Challenges: border cases add latency, so keep shard lines away from dense areas (split along rivers and city limits, not through downtown). A globally consistent database costs write latency for cross-region commits; keep each ride's data homed in its region to keep most commits local.

Fig. 15

Shard by geography: a city's rides stay in its region

Shard by geography: a city's rides stay in its regionA geo router at the top maps the pickup cell to a region. Three regional zones (us-east for New York, eu-west for London, ap-south for Mumbai) each hold their own Matching service, geo index and ride data. Notes: a rider near a shard line queries both shards and merges (scatter-gather only at boundaries), and ride data lives in a strongly consistent store, as Uber did when it moved from Ringpop and Cassandra to Spanner.Geo Routerpickup cell → regionus-east · NYCMatchingGeo indexRidesSpannereu-west · LondonMatchingGeo indexRidesSpannerap-south · MumbaiMatchingGeo indexRidesSpannerNear a shard line, ring 2 crosses into the neighbour:query both shards, merge, rank.Rides: transactional, strongly consistent(Uber: Ringpop + Cassandra → Spanner)
Shard by geography. Each region holds its own matching, geo index and ride data. Only searches that cross a shard line fan out to the neighbour.
Uber's fulfillment platform used to route each entity to an owning Node.js worker with

Ringpop and store it in Cassandra, favouring availability, with Sagas for multi-entity changes. The rewrite moved rides and supply to Google Cloud Spanner for transactions across rows and tables with external consistency (Uber). The lesson to say out loud: for the booking itself, pay for strong consistency.

055. The final design

Here's everything on one board: the high-level design plus every deep dive.

Fig. 16

Design Uber: the whole platform on one board

Design Uber: the whole platform on one boardLeft to right: rider and driver apps, an edge tier, core services, data stores and third parties. The rider app reaches a load balancer and API gateway. Core services: a Fare service that calls a maps API, the Ride service that owns ride state, a ride events stream feeding Payments and the payment provider, the Location service, a Kafka ride queue partitioned by zone, the ETA service, Matching, an offer workflow on Cadence, and a Notification service that pushes through the push gateway and APNs or FCM. Data stores: a Redis geo index keyed by H3 cell, a Spanner ride store and Redis driver locks. Numbered steps follow one ride: request, store, enqueue, consume, nearby cells, rank by ETA, lock, offer, accept, and the GPS stream that keeps the index fresh.ClientsEdgeCore services (per region)Data storesThird partiesRiderDriverLoad balancerL7 · TLSAPI gatewayauth · rate limitPush gatewaygRPC streamsFare servicequotes · surgeRide serviceride state machine...Ride eventsPaymentscharge on finishLocation servicepings in...Ride queue · by zoneETA serviceroad graph + trafficMatchingconsumers · by zoneOffer workflowCadence · 10 s timersNotificationwho to tell, whenGeo indexRedis · H3 cellsRide storeSpannerDriver locksRedis · SET NXMaps APIroutes · ETAAPNs / FCMmobile pushPSPcards · wallets1route + ETAinsert REQUESTED234move cell5rank by ETA67offer8accept9GPS10chargeLegendrequestevent (async)data read / writeFollow one ride1Rider requests a ride with a fareId2Ride service stores it as REQUESTED3The request is queued by pickup zone4A matcher for that zone consumes it5Available drivers from nearby H3 cellsThen6Rank candidates by real ETA7Lock the best driver for 10 s8Offer pushed down the driver's stream9Driver accepts: one conditional write10GPS keeps the geo index fresh
The whole platform. Follow badges 1 to 10: the ride is stored and queued by zone, a matcher reads nearby cells and ranks by ETA, the offer workflow locks one driver and pushes the offer, and the accept is one conditional write. GPS from the driver app keeps the geo index fresh the whole time.

One ride, end to end:

  1. The rider requests a ride with a fareId; the load balancer and API gateway route it to the Ride service.
  2. The Ride service stores the ride as REQUESTED in the Spanner ride store (idempotent on fareId).
  3. It writes the request to the Kafka ride queue, keyed by pickup zone.
  4. The matcher that owns that zone's partition consumes it.
  5. It reads available drivers from the nearby H3 cells in the Redis geo index.
  6. It ranks them by ETA with the ETA service.
  7. The offer workflow (Cadence) locks the best driver in Redis for 10 s.
  8. The Notification service pushes the offer down the driver's open stream through the push gateway (APNs/FCM if the app is in the background).
  9. The driver accepts through the API gateway; the Ride service runs the conditional transaction, and the rider is told the same way the driver was.
  10. All the while, the driver's GPS flows through the push gateway to the Location service, which moves the driver between cells in the geo index.

066. What interviewers look for by level

Mid-level

  • A clean API with the fare quote as a server-side resource, and identity from the JWT.
  • A working high-level design that covers all four requirements.
  • Knowing that "nearby" needs a geospatial index (geohash, quadtree or H3), not a plain table scan.

Senior

  • Separating the location stream from ride bookings, with numbers to justify it.
  • An in-memory geo index with stale-driver cleanup.
  • A lock with a TTL and a conditional transaction against double offers, and why you need both.
  • A durable queue for requests with commit-after-match, and push instead of polling.

Staff+

  • Treating the ping rate as a design lever (adaptive intervals with a server override).
  • Durable workflows for offer timers, with a clear failure story at every step.
  • Geo-sharding with one consistent key, scatter-gather only at borders, and a reasoned consistency choice per data type, backed by what Uber actually did.

077. Follow-up questions

What if we offer the ride to three drivers at once?

Matching gets faster in areas where drivers often ignore offers, because you don't wait out three 10 s timers in a row. The cost is drivers seeing offers they can't win. The design already handles it: lock three drivers, and the conditional transaction lets the first accept win while the others get a "ride taken" message. I'd use it only where acceptance rates are low.

What if a Redis geo shard goes down?

The index is rebuildable state. Promote the replica; any drivers it missed re-appear with their next ping, within 5 to 30 s. During the gap, matching in that region widens its search or retries for a few seconds. That's the point of keeping rides out of Redis.

What if the driver loses signal mid-trip?

The trip keeps going. Mark the location stale after ~30 s and show the rider the last known point with a "reconnecting" note. The app buffers pings and uploads them on reconnect, so the recorded route, and the fare computed from it, stay correct.

What if the rider cancels while an offer is out?

The cancel is a PATCH that flips the ride from REQUESTED to CANCELLED with the same conditional update, and it sends a signal to the ride's workflow. If the driver accepts a moment later, the accept transaction finds the ride isn't REQUESTED and returns 409. Whoever commits first wins, and there's no third outcome.

How would you add surge pricing?

Count open requests (demand) and available drivers (supply) per H3 cell every minute from the ride and location streams, and turn the ratio into a multiplier per cell. The Fare service reads it when quoting, and the multiplier is saved on the Fare, so the rider pays what they saw even if surge changes before they confirm.

088. How to say it in the interview

When I ask this question, I'm listening for three things: do you keep the location stream away from the bookings, can you find nearby drivers without a scan, and can you explain why a driver can't get two rides.

In the interview

"I'll quote fares as a stored resource, so a ride request only carries a fareId and the price can't be faked. Drivers ping roughly every 5 s, about 400k writes a second at 2M online drivers, so locations go to an in-memory geo index sharded by region, Redis GEOSEARCH or H3 cells, never the ride database, and drivers silent for 30 s drop out. The app adapts its ping rate: fast near a pickup, slow when parked. Ride requests go onto Kafka keyed by pickup zone, and I commit the offset only after a match, so a stadium spike becomes lag, not lost requests. A matcher reads nearby available drivers, ranks them by ETA, and locks the best one in Redis with SET NX and a 10 s TTL so no one else offers her. A durable workflow, like Cadence, owns the 10 s timer and moves to the next driver on timeout. Accept is one conditional transaction in a strongly consistent store like Spanner: ride still requested, driver still free, or 409. Updates go to the apps over a persistent stream with sequence numbers, falling back to APNs and FCM in the background, because polling was once 80% of Uber's gateway traffic."

On the whiteboard

  • FRs: fare estimate, request ride, match driver, accept + navigate
  • NFRs: match < 1 min or fail, no double offers, stadium spikes, fast location ingest
  • API: POST /fares, POST /rides {fareId}, POST /drivers/location, PATCH /rides/:id (409)
  • Locations: ~400k writes/s → Location svc → Redis geo / H3 cells per region, drop after 30 s
  • Adaptive pings: 2 s near pickup, 5 s driving, 30 s parked
  • No double offers: SET lock NX PX 10000 + conditional commit on accept
  • Spikes: Kafka keyed by zone, commit after match, autoscale on lag
  • Timeouts: durable workflow (Cadence / Temporal), 10 s timers survive crashes
  • Push: persistent stream + seq numbers; APNs/FCM fallback; no polling
  • Scale: geo-shard by region, scatter-gather at borders, rides in Spanner

Concepts used in this design

Go deeper in Articles

Optional long reads behind this design, for when you want the full story.

Further reading (7)