Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

System Design

Design Uber

Learn system design by building a ride-hailing service like Uber step by step.

The numbers to beat~5Mactive driversevery 4seach pings~1M+pings / second

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What is Uber, really?

Strip away the brand and Uber does one deceptively simple thing: connect a person who wants a ride with a nearby car — in seconds, anywhere, all at once.

Rider AppDriver App
New in this step: Rider App, Driver App. · swipe to pan the diagram

That tiny sentence hides three brutally hard problems. Where is everyone right now? Who is the best match? And how do we do this for millions of people at the same time without falling over?

What the new pieces do

Rider Appclient
The passenger's phone. Asks for a ride, watches the car approach, pays at the end.
Driver Appclient
The driver's phone. Streams its GPS location constantly and receives ride offers.

Step 1 · The skeleton

Two apps and a server

The rider’s phone and the driver’s phone can’t talk to each other directly — phones move, change networks, and can’t be trusted to coordinate fairly. How should they connect?

Backend ServerDatabase
New in this step: Backend Server, Database. · swipe to pan the diagram

How do the two phones coordinate a ride?

  1. Phones drop off networks, lie about position, and can’t see the global pool of drivers. Fairness, pricing and trust all need a referee neither phone can be.

  2. A neutral backend holds the single source of truth, makes the matching decision, and survives any one phone going offline. This is the spine of every app you use.

  3. Letting clients touch the database means no business rules, no auth boundary, and every phone trusting every other phone’s writes — a security and consistency nightmare.

We put a server in the middle. Both apps send requests to it; it does the thinking and remembers everything in a database. This is the client → server → database pattern under every app you use.

Why this piece earns its place

The thing this skeleton really buys is a trust boundary, and it is worth saying out loud, because every number downstream is one a phone sent us. A driver app can report a position it is not at; a rider app can ask to be billed for a trip it did not take. So the server does not merely decide, it verifies: a location ping counts only for the driver whose authenticated session sent it, and the fare is computed from what the server recorded, never from a total the client submits. The second thing to build into this first endpoint is retries. Mobile networks drop mid-request constantly, so the app will resend a ride request it is not sure landed, and a naive server cheerfully creates two. Give the request a client-generated id and make the create conditional on it, so a retry returns the original request instead of a duplicate. Both of these cost almost nothing while there is one server and one endpoint, and are miserable to retrofit once five services are calling it.

What the new pieces do

Backend Serverbackend
The single front door for every app request. Later it becomes an API Gateway sitting behind a load balancer.
Databasestore
Durable storage for things you must never lose: users, trips, receipts, payments.

Step 2 · Eyes on the road

Where is everyone right now?

To match a rider with a car, we need to know where every driver is — and they’re moving. A location from 2 minutes ago is useless. Where does a position that changes every few seconds belong?

Location Serviceingest pingsLocation StoreRedis · RAM
New in this step: Location Service, Location Store.

~5M drivers each ping their GPS every ~4s. Where do you keep "current position"?

  1. That’s ~1M+ writes/sec of data you overwrite seconds later. Disk-backed transactional storage would melt — and you’d be paying durability guarantees for data you never need to recover.

  2. Live positions are tiny, change constantly, and don’t need to survive a crash. RAM gives microsecond writes; you keep only "now", not history. Storage matched to behaviour.

  3. You’d have to poll millions of phones at match time, many asleep or offline. Pull-on-demand turns one fast lookup into millions of slow round-trips.

Each driver app sends a GPS ping every ~4 seconds to a dedicated Location Service. It overwrites the driver’s "current position" in a super-fast in-memory store (Redis). We don’t keep history — we only ever care about now.

Why this piece earns its place

The ping interval is the knob this step introduces, and it sets nearly everything else on the path. At every 4 seconds across ~5M drivers we are already at roughly 1.25M writes per second, and that figure is linear in the rate: halve the interval and you double the write volume, double the shard count, and double the battery and cellular data you burn on a phone clamped to a windscreen all day. Stretch it the other way and the cost lands somewhere less visible — the position we match on, the arrow on the rider’s map and the arrival estimate are all as old as the interval, so a car at motorway speed sits a long way from where the system thinks it is. The way out is to stop treating it as one global number, though the floor is never silence. A driver who is off duty is not in the pool at all and can ping as slowly as we like; an idle driver waiting for work is exactly the supply the matcher queries, so that one still has to stay inside the freshness window the geo index enforces — a cheap heartbeat, enough to keep the position live and no more. A driver mid-pickup with a rider watching the map gets the fastest rate we have. Set the rate from the driver’s state and the write budget gets spent where somebody is actually looking.

  • ~5Mactive drivers
  • every 4seach pings
  • ~1M+pings / second

What the new pieces do

Location Serviceservice
Receives a GPS ping from every active driver every few seconds and writes their latest position.
Location Storestore
An in-memory store (like Redis) holding each driver’s current position. Fast to write, fast to read.

Back of the envelope

5M drivers ÷ 4s
≈ 1.25M location writes per second
~100 bytes / ping
tiny, fixed-size — perfect for RAM
keep 1 value per driver
O(drivers), not O(time) — history is thrown away
Redis does ~100k ops/s/node
⇒ shard the location store across ~10s of nodes

Step 3 · The clever bit

Who’s near me?

A rider taps the screen. We can’t scan all 5 million drivers to find nearby ones — that would take forever, on every single request. How do you make "who’s near this point?" fast?

Location Serviceingest pingsLocation StoreRedis · liveGeo IndexH3 / quadtree
New in this step: Geo Index.

A rider needs nearby drivers. How do you avoid scanning all 5M of them?

  1. That’s 5M distance calculations per request, on every request. Linear scans don’t survive contact with scale — this is exactly what indexes exist to kill.

  2. Latitude alone ignores longitude — nearby in one axis can be a continent away in the other. 2-D proximity needs a 2-D structure, not a 1-D sort.

  3. A spatial index (Uber’s H3 hexagons, or a geohash/quadtree) turns "search everyone" into "look in my cell + its neighbours" — a few dozen drivers instead of millions.

We chop the world into small cells (Uber uses hexagons called H3; the textbook version is a geohash or quadtree). Every driver lives in a cell. "Near me?" becomes "who’s in my cell and the 6 around it?" — a handful of cells instead of millions of drivers.

Why this piece earns its place

The average hides the case that hurts. Spread the fleet over the cells and you get a handful of drivers per populated cell, which is the number that makes this look free — but density is nothing like uniform. An airport rank, a stadium at closing time, a city centre on a Friday night packs hundreds of drivers into one cell, and that is precisely the cell everyone is querying at once. Fixed cells make the work per query a property of the neighbourhood rather than of our code. Two defenses. First, do not treat the cell as the answer: pull candidates and cap how many get scored, because the rider does not need the best of six hundred, only a good one of a few dozen. Second, search outward instead of at a fixed radius — home cell, then the ring of neighbours, stopping as soon as there are enough candidates — so a sparse suburb widens until it finds cars while a dense centre stays tight, with nobody hand-tuning a radius per city. The write path deserves a look too: a driver near a cell boundary changes cell on almost every ping, so the index entry wants overwriting in place rather than a delete and an insert.

What the new pieces do

Geo Indexindex
A spatial index that answers 'who is near this point?' in milliseconds by bucketing the map into cells.

Back of the envelope

globe → ~millions of H3 cells
each ~1 km² at the resolution Uber dispatches on
5M drivers / cells
≈ a handful of drivers per populated cell
query = 1 cell + 6 neighbours
bounded work regardless of fleet size

Step 4 · The match

Pick the right driver

Five cars are nearby. Which one gets the trip? Closest isn’t always best — a car 2 min away facing the wrong way down a highway loses to one 3 min away pointed right at you. How do you choose?

Backend ServerMatching ServiceGeo Index
New in this step: Matching Service. · swipe to pan the diagram

The geo index returns 5 nearby drivers. Who gets the trip?

  1. "As the crow flies" ignores one-way streets, rivers, and which way the car is pointed. The nearest dot on a map is often a longer real drive than one further out.

  2. Dispatch optimizes the thing riders actually feel — time-to-pickup — and respects driver choice. Decline → instantly offer the next best. This is the heart of the product.

  3. A free-for-all means four drivers wasted, race conditions on who "won", and incentives to spam-accept. Dispatch must make a deliberate, fair, single offer.

The Matching Service (Uber calls it dispatch) asks the geo index for nearby drivers, scores them on real ETA, direction, and rating, then offers the trip to the best one. If they decline, it instantly offers the next.

Why this piece earns its place

Scoring introduces two dials, and both are tuned with data rather than taste. The first is how long an offer stands before dispatch gives up and moves on. Too long and the rider watches a spinner while the second-best car drives out of range; too short and we pull offers from drivers who are mid-turn and physically cannot reach the phone, which feels to them like being skipped and is one of the quieter reasons good drivers stop accepting at all. The second is the weighting between time-to-pickup, heading and rating. Lean hard on rating and a new driver never gets a first trip, so the supply the whole product depends on never builds. Lean entirely on time-to-pickup and the optimal driver behaviour becomes parking in the busiest cell and idling there, which is not the incentive we want to ship. And put a floor under the fallback chain: after a few declines the world has moved, so re-run the nearby query rather than working down a candidate list assembled tens of seconds ago.

What the new pieces do

Matching Serviceservice
The brain. Given a rider’s location, finds nearby free drivers and picks the best one to offer the trip to.

Step 5 · Don’t fall over

Now do it 25 million times a day

One server was fine for a demo. At real scale it melts: too many requests, and if it dies, all of Uber goes dark. How do you carry 25M trips a day and survive a machine dying?

Rider AppDriver AppAPI GatewayLocation ServiceMatching ServiceLocation StoreGeo IndexDatabase
The system as it stands at this step. · swipe to pan the diagram

One server can’t hold 25M trips/day and is a single point of failure. What now?

  1. There’s a hard ceiling on the biggest machine money buys — and it’s still one machine. When it dies, all of Uber dies with it. Vertical scaling buys time, not safety.

  2. Replicas help reads, but the single app server is still the bottleneck and the single point of failure. You’ve scaled memory, not the thing that’s actually melting.

  3. Run many small stateless instances behind a load balancer, split into microservices so location, matching and trips scale — and fail — independently. One dead box no longer takes the system down.

Put a load balancer in front and run many copies of each service (horizontal scaling). The single server becomes an API Gateway that routes traffic. Split work into independent microservices so location, matching, and trips can scale — and fail — on their own.

Why this piece earns its place

Splitting one server into many services trades one failure mode for a harder one. A single process is up or down; a fleet talking over a network is mostly up, and mostly up is the state that produces the confusing outages. Every hop a ride request now makes needs its own deadline and its own answer for what happens when that deadline passes, and the answer is rarely to keep waiting — a matcher blocked for seconds on a slow index lookup is holding a connection that a client retry is about to duplicate. Give each call a budget and prefer a worse answer on time to a perfect one late. The other thing to get right early is what the load balancer is actually asking. It routes around a dead box only if it can tell the box is dead, and a health check that returns OK because the process is running will happily keep feeding dispatch traffic to an instance that has lost its connection to the index. Make the check exercise the dependency the instance cannot work without, and have the instance pull itself out of rotation rather than serve errors from inside it.

  • 130M+monthly users
  • 25M+trips / day
  • 10k+servers

Back of the envelope

25M trips/day ÷ 86,400s
≈ 290 trips/sec average
× ~5 peak factor
≈ 1,500 dispatches/sec at rush hour
+ 1.25M location writes/sec
the location path dwarfs the trip path — scale them apart
stateless instances
add boxes linearly; the LB spreads load and routes around the dead

Step 6 · Make it feel alive

The car on the map that moves

After matching, the rider stares at a map watching the car approach. Asking the server "where’s my car now?" every second, for everyone, would crush it. How do live updates reach the phone?

API GatewayRealtime GatewayMatching ServiceGeo IndexDatabaseEvent Stream
New in this step: Realtime Gateway, Event Stream. · swipe to pan the diagram

The rider wants the car to move on the map in real time. How?

  1. Millions of phones each hammering an HTTP request per second is a self-inflicted DDoS — mostly to hear "nothing changed". Polling wastes a request for every non-event.

  2. An always-open connection lets the server talk first — it sends a new position the instant it arrives, no request needed. Meanwhile services emit events to Kafka so they don’t block each other.

  3. Wrong channel entirely — latency in seconds-to-minutes, rate limits, and cost per message. Push notifications and SMS are for the app being closed, not a live map.

Open one persistent connection (a WebSocket) per phone through a Realtime Gateway, so the server can push updates the instant they happen. Meanwhile every service writes events to a Kafka stream — a buffer that lets services react without waiting on each other.

Why this piece earns its place

A persistent connection is state, and it is the one piece of this system that does not come back gracefully. When a gateway instance dies or we roll a new version, every phone it was holding notices at the same instant and reconnects at the same instant — and a reconnect is not a cheap request, it is a handshake plus an authentication plus re-subscribing to whatever that phone was watching. Millions arriving in one second will take out the replacement instance and then the one after it. Two things stop that: random jitter on the client’s reconnect delay so the herd spreads itself, and deploys that drain a small share of connections at a time instead of restarting a whole tier. The second habit is being honest about what a push guarantees, which is nothing. A dropped position update does not matter; another is seconds behind it. A dropped “your driver has arrived” does. So anything the rider must not miss has to be re-readable from the trip’s own state when the socket comes back, with the push treated as an optimisation over a state the phone can always fetch again.

What the new pieces do

Realtime Gatewaypush
Holds an always-open connection to each phone so the server can push updates (car moved!) instantly — no refreshing.
Event Streambus
A firehose every service writes events to. Decouples services and feeds analytics, surge pricing, fraud checks, etc.

Step 7 · The trip & the money

From 'requested' to 'paid'

A trip isn’t one moment — it’s a journey through states, and money is involved. Lose a payment or double-charge someone and trust evaporates. How do you model the ride and handle the cash?

Trip Servicetrip statePayment Servicebilling
New in this step: Trip Service, Payment Service.

A trip moves requested → accepted → in-progress → completed, then bills. How do you build it?

  1. Welding billing onto dispatch means a payment bug can stall matching, and a traffic spike on rides can drop charges. Money needs isolation, not a seat on the hot path.

  2. A state machine makes illegal transitions impossible; SQL gives the transactional guarantees money demands; isolating payments means billing bugs can’t break dispatch. Right tool, right job.

  3. Trips and receipts must survive crashes and be auditable for years — the exact opposite of disposable live positions. This is durable, transactional data, not hot-path cache.

A Trip Service walks each ride through a strict state machine and stores it in a reliable SQL database, sharded by city so Delhi’s load never touches London’s. A separate Payment Service handles fares and payouts so billing bugs can’t break dispatch.

Why this piece earns its place

A state machine is a guarantee only if transitions are the single way state moves and each one is conditional. The race to plan for is two genuine events landing together: the driver taps “arrived” in the same moment the rider cancels. If both handlers read the current state and then write it, the slower write silently wins, and we end up with a trip that was cancelled and is also in progress, with the billing decision attached to whichever request happened to finish last. Write each transition as an update guarded on the state we expect to find, so the loser fails loudly and can be told what actually happened, and hang every side effect — the notification, the charge — off the transition that committed rather than off the request that asked for it. The other half is making sure every trip can reach an end without a human. A driver who finishes the ride and never taps complete leaves an open trip and an unbilled fare indefinitely, so the machine needs its own timeout path into a resolved state, or we quietly accumulate rides nobody can close and nobody is watching.

What the new pieces do

Trip Serviceservice
Tracks a trip through its lifecycle: requested → accepted → arriving → in progress → completed.
Payment Serviceservice
Calculates fare, charges the rider’s card, and pays out the driver after the trip ends.

You did it

You just designed Uber.

Rider AppDriver AppAPI GatewayRealtime GatewayLocation ServiceMatching ServiceTrip ServicePayment ServiceLocation StoreGeo IndexTrips DBEvent Stream
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Client → server → database — the skeleton every app shares.
  • Live driver positions in fast in-memory storage (Redis).
  • Geospatial index (H3) turns 'who’s near me' into a tiny lookup.
  • Matching service scores drivers and dispatches the best.
  • Load balancer + microservices to survive 25M trips a day.
  • WebSockets push live updates; Kafka decouples services.
  • Trip state machine + sharded SQL + isolated payments.

Deep cut · 15:02

One tap, one rainy night, five million cars.

The interactive build above lays out ride-hailing dispatch around one request: latest positions held in memory, a hexagon cell index that shrinks five million cars to a few dozen, road minutes instead of straight-line distance, a matcher that pairs riders together, a live push line, and a trip ledger that charges once. The film follows that one tap from the pin to the curb, then finds the seventh part when a stadium empties.

  • See the numbers: 5,000,000 cars pinging every 4 s is 1,250,000 updates a second. Seven hexagon cells of about six cars each leave 42 candidates, 119,048 times fewer than a scan.
  • The river trap: a car 0.5 km away in a straight line is 4.0 km by road (8 minutes), while one 0.6 km away is 0.7 km by road (1.4 minutes). Rank by time, not distance.
  • Take it into the interview: pair riders together (6 minutes in total against 11 greedy), push instead of poll, a version check and a charge key (25,000 double charges a day without one), and a price dial that balances a crowd.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Surge pricing — where does it live?

    It’s a consumer of the event stream, not part of the hot match path. A pricing service watches demand-vs-supply per geo cell off Kafka and publishes a multiplier the matching/quote path reads. Keeping it asynchronous means a pricing hiccup can’t stall dispatch.

  2. Two riders request the last nearby driver at once — who wins?

    Dispatch must treat a driver as a lockable resource: the offer places a short-lived hold (a lease in Redis or a conditional state transition) so a driver can only be offered one trip at a time. The loser is instantly re-matched to the next-best car.

  3. A driver’s pings stop — dead zone or app crash. What happens?

    Positions carry a TTL, so a stale driver simply ages out of the geo index and stops being matched. On reconnect the app resumes pinging and re-registers. Never dispatch on a location older than a few seconds.

  4. Why shard trips by city instead of by user ID?

    A ride is overwhelmingly a within-city event — rider, driver, dispatch and pricing all live in one metro. City sharding keeps a trip’s reads and writes on one shard (no cross-shard joins) and lets you scale hot cities independently. Cross-city is rare enough to handle as a special case.

  5. How do you stop a rider being double-charged?

    Payments are idempotent: each trip generates one payment intent keyed by trip ID, so a retried charge collapses onto the same record. The Trip Service only emits "charge" once, on the completed → billed transition, and the Payment Service dedupes on that key.

Check yourself — the answers, and why

Eight steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Live driver positions are kept in Redis because they’re…

    Only "now" matters and a crash losing it is harmless — so RAM, not a durable disk DB.

  2. "Who’s near me?" is fast because of…

    Bucketing the map into cells turns millions of comparisons into a few-cell lookup.

  3. The car moving live on the rider’s map is delivered by…

    A persistent connection lets the server push the instant a position changes — no polling.

  4. Trips and payments live in SQL (not Redis) because they’re…

    Money and receipts must survive crashes and stay correct — the opposite of hot-path cache.

  5. Matching is its own microservice mainly so that…

    When the matcher dies, browsing, live tracking and in-progress trips all keep working.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Request a ride: a rider asks and gets matched to a nearby car in seconds.
  • Ingest location: every active driver streams GPS; the system tracks each car’s current position.
  • Find nearby: answer “which free drivers are near this point?” in milliseconds.
  • Match & dispatch: score nearby drivers and offer the trip to the best one, falling back if declined.
  • Trip & billing: walk a ride requested → completed and charge the fare exactly once.

The qualities that shape everything

Each one names the mechanism that buys it.

Absorb ~1M+ location writes/sec cheaply
Overwrite each driver’s current position in an in-memory store (Redis) and keep no history — the data is tiny, disposable, and only “now” matters.
“Who’s near me?” without scanning 5M drivers
A geospatial index (H3 cells / geohash) buckets the map so a nearby-search is “my cell + its neighbours” — a few dozen drivers, not millions.
Match on real time-to-pickup, fairly
The Matching Service scores nearby drivers on ETA, heading and rating and makes one deliberate offer, re-offering the next-best on decline.
Survive a machine dying at 25M trips/day
A load balancer fronts many stateless microservice instances, so location, matching and trips scale — and fail — independently.
A live map without a polling storm
A persistent WebSocket per phone lets the server push position updates the instant they arrive; Kafka decouples services so a slow one can’t block a fast one.
Money is durable and correct
A Trip Service runs a strict state machine in sharded SQL, and a separate Payment Service bills, so a billing bug can’t break dispatch.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Live positions in Redis over the main SQL database

5M drivers pinging every ~4s is over a million writes/sec of data you overwrite seconds later — disk-backed transactional storage would melt, and you’d pay durability for data you never recover. RAM matches the behaviour: keep only “now”, throw history away.

A geospatial cell index (H3) over computing distance to every driver

5M distance calculations per request doesn’t survive contact with scale, and a 1-D latitude sort ignores longitude — nearby in one axis can be a continent away in the other. Bucketing the map into cells turns “search everyone” into a few-cell lookup.

WebSocket push over the app polling every second

Millions of phones each firing an HTTP request per second is a self-inflicted DDoS, mostly to hear “nothing changed.” A persistent connection lets the server talk first and send a new position the instant it arrives.

A trip state machine in SQL with isolated payments over welding billing onto dispatch

A payment bug on the live dispatch path can stall matching, and a ride spike could drop charges. A strict state machine in SQL gives money the transactional, auditable guarantees it needs, and isolating payments keeps billing bugs off the hot path.

The answer, out loud

What a strong answer to “Design Uber (Ride-Hailing)” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Scope it before I draw anything

    Uber is one sentence — connect somebody who wants a ride with a nearby car, in seconds, everywhere at once — and nearly all the difficulty hides inside three questions. Where is everyone right now, who is the best match, and how do I do that for everybody simultaneously. What I want on the table early is that this is really two systems with different shapes. There is a firehose of driver locations, enormous and disposable, and there are trips and money, comparatively rare and impossible to lose. Build both with the same tools and I will get one of them badly wrong.

  2. 3–8 min

    The skeleton, and who I trust

    The skeleton is deliberately boring: both phones talk to a server, and the server remembers things in a database. I am not letting the phones talk to each other. Matching has to see the whole pool of drivers, fairness needs a referee neither phone can be, and a phone can go offline mid-ride without the ride being lost. And since everything downstream is computed from numbers a phone sent me, the server verifies rather than accepts — a ping counts only for the authenticated driver who sent it, and I compute the fare rather than taking one from the client.

    Built in step 1: Two apps and a server
  3. 8–14 min

    The location firehose

    Now the locations. Around five million drivers each pinging every four seconds is over a million writes a second, and I want to describe that data before choosing where to put it. It is about a hundred bytes, it is overwritten seconds later, and if a crash loses all of it nothing is destroyed — the drivers are still out there and they will ping again. That is not database data. So it lives in memory, in Redis, one value per driver, overwritten in place, no history. A Redis node does on the order of a hundred thousand operations a second, so this shards across a few dozen nodes.

    Built in step 2: Where is everyone right now?
  4. 14–20 min

    Who is near this rider

    Next, the question the product is built on: who is near this rider. I am not computing five million distances per request, and I am not sorting by latitude, because being close on one axis says nothing about the other. Instead I bucket the map into cells — Uber uses hexagons called H3, a geohash or a quadtree does the same job — and every driver is filed under the cell they are in. Nearby becomes my cell plus the ring around it, a few dozen candidates rather than millions, and the work per query stops growing with the fleet.

    Built in step 3: Who’s near me?
  5. 20–27 min

    Picking the driver

    Then dispatch chooses. Nearest in a straight line is the intuitive answer and it is wrong often enough to matter — a car two minutes away facing the wrong way down a dual carriageway is a worse pickup than one three minutes away pointed at you. So matching scores candidates on real time-to-pickup, heading and rating, and makes one deliberate offer to the best of them, falling to the next on a decline. I am not broadcasting to all five and letting them race: that wastes four drivers and teaches everyone to accept first and think afterwards.

    Built in step 4: Pick the right driver
  6. 27–32 min

    Making it survive a bad day

    At twenty-five million trips a day one box is both too slow and the whole company’s single point of failure, so I scale out rather than up — a load balancer in front, many stateless copies of each service behind it, split so location, matching and trips scale and fail on their own. That split is the important half. Averaged out this is a few hundred dispatches a second, maybe five times that at rush hour, which is tiny beside the million-plus location writes. Those two paths have no business sharing a fleet.

    Built in step 5: Now do it 25 million times a day
  7. 32–37 min

    The car that moves on the map

    Once a rider is matched they sit watching a car approach, and I am not serving that by having every phone ask once a second whether anything changed — millions of requests a second, nearly all to hear nothing. Instead each phone holds one persistent WebSocket through a realtime gateway and the server pushes a new position the instant it has one. Behind the services I put an event stream, Kafka, so everything that wants to watch this reads at its own pace instead of slowing dispatch down.

    Built in step 6: The car on the map that moves
  8. 37–42 min

    The trip and the money

    The last piece is the ride itself and the cash. A trip is a state machine — requested, accepted, arriving, in progress, completed — and I keep it in SQL, sharded by city, because a ride happens inside one metro and that keeps its reads and writes on a single shard. Payments sit in their own service rather than welded onto dispatch. A billing bug should be able to break billing without stopping anybody getting a car, and a spike in rides should never drop a charge.

    Built in step 7: From 'requested' to 'paid'
  9. 42–45 min

    Where the trade-off sits

    So, in one breath: a server in the middle, live positions in memory, a cell index so nearby is cheap, a matcher making one scored offer at a time, load-balanced services, sockets for the live map, and a state machine with isolated payments. The trade-off running through all of it is that I spent durability to buy speed on the location path and spent speed to buy correctness on the money path, and it works only because those genuinely are two systems. With more time I would go at two things I have been quietly assuming: the time-to-pickup estimate itself, which is a routing problem over a live road graph, and cancellations — who pays when a rider backs out after a driver has already driven toward them.

What this teaches

Learn system design by building a ride-hailing service like Uber step by step. An interactive guide covering the client–server skeleton, live driver-location ingestion, geospatial indexing, matching and dispatch, horizontal scaling, realtime push over WebSockets, and the trip state machine with isolated payments.

Key takeaways

  • Client → server → database — the skeleton every app shares.
  • Live driver positions in fast in-memory storage (Redis).
  • Geospatial index (H3) turns 'who’s near me' into a tiny lookup.
  • Matching service scores drivers and dispatches the best.
  • Load balancer + microservices to survive 25M trips a day.
  • WebSockets push live updates; Kafka decouples services.
  • Trip state machine + sharded SQL + isolated payments.

Concepts covered

  • What is Uber, really?
  • Two apps and a server
  • Where is everyone right now?
  • Who’s near me?
  • Pick the right driver
  • Now do it 25 million times a day
  • The car on the map that moves
  • From 'requested' to 'paid'
built to be driven, not memorized — make the calls, kill the matcher, run the gauntlet.
Finished this one? 0 / 65 System Designs done
NextDesign a Payment System Next in “System Design in 30 Days” · step 20 of 21

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs