System Design

Design Uber

Step 1 / 9

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

Deep cut · 13:23

Watch the whole system derived from the numbers that force it

The walkthrough gives you the components. This masterclass builds them in order from the requirements: why one table and a sort is genuinely correct until 1.25 million writes a second kill it, why an index on latitude returns a car nine hundred kilometres away, and why the search never happens at all — the map is cut into fixed cells before anybody asks.

  • Watch the naive build die honestly: one table, one sort — correct at small scale, then broken by the write rate, the full scan, and the second coordinate an index cannot cover.
  • Take it into the interview: positions in memory, a cell index, drive-time scoring, a load-balanced service split, one open connection per phone and a trip state machine — each priced with what it buys and what it costs.

In the interview room

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.

Functional requirements

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.

Non-functional requirements

The qualities that shape the whole design — 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 Redisover 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 pushover 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 paymentsover 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.

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'

Design Uber (Ride-Hailing) — read the full walkthrough as text

the same steps, decisions & trade-offs, for reading, reference & search

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.

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?

How to read this: Each step opens with a real design decision — you make the call before I show you what ships. Then watch the diagram on the right grow. Hover any box, replay the flow, and at the end kill the matcher to see what breaks. Hit Begin.

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?

Design decision: How do the two phones coordinate a ride?

The call: A server in the middle decides, and a database remembers. — 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.

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.

Client–server model: The phones are clients (they ask). The server is the brain (it decides). The database is memory (it never forgets). Keep these three roles separate and everything else gets easier.

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?

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

The call: Overwrite the latest position in an in-memory store (Redis). — 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.

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.

Hot path vs durable data: Live positions change constantly and don’t need to survive a crash, so they live in RAM (Redis), not a slow disk database. Match the storage to how the data behaves.

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?

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

The call: Bucket the map into cells and only look at nearby cells. — 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.

Geospatial indexing: An index turns "search everything" into "look in the right drawer." By indexing drivers by location cell, a nearby-search drops from millions of comparisons to a few dozen.

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?

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

The call: Score on real ETA, heading and rating; offer to the best, fall back if declined. — 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.

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.

This is the heart: Everything else exists to feed this decision. A good match means a short wait for riders and steady earnings for drivers — the core promise of the whole product.

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?

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

The call: Load balancer + many copies of each service (scale out). — 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.

Scale out, not up: Instead of one giant machine ("scale up"), run many small ones behind a load balancer ("scale out"). Cheaper, and one dying machine no longer takes the system with it.

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?

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

The call: One persistent WebSocket per phone; the server pushes updates. — 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.

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.

Push & decouple: WebSockets flip the model: the server talks first. Message queues like Kafka let a slow service catch up later instead of blocking a fast one — the system bends instead of breaking.

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?

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

The call: A Trip Service runs a strict state machine in SQL; a separate Payment Service bills. — 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.

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.

Right tool, right job: Live locations → fast in-memory store. Money & trips → strict SQL with guarantees. Sharding splits one giant database into per-city pieces so each stays small and fast.

You did it

You just designed Uber.

  • 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.
built to be driven, not memorized — make the calls, kill the matcher, run the gauntlet.
Finished this one? 0 / 65 System Designs done

Explore the topic

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

More System Designs