Ride Sharing
H3 geo-indexing, DISCO batched matching, surge pricing, city-sharded state.
The scenario
Ride matching at $162B GB — where H3 geospatial index and DeepETA converge
Same startup, same engineer #4. Tenth Monday.
Your CTO drops by. "Users love everything we've built. Now the ambitious ask — we want to add a ride-hailing feature. A rider requests, a driver accepts, GPS tracks the trip, payment happens automatically. Ship an MVP in 12 weeks."
She pauses. "For context — Uber processed $162 billion in gross bookings in 2024 across ~161M monthly active platform consumers (MAPCs — Uber's official metric, not MAU). They invented H3, a hexagonal geospatial index that's now open-source and used by anyone doing serious geo work. And their DeepETA model — a deep neural network that predicts trip ETAs — reduced error by 50% vs classical routing algorithms. If we're going to compete, we need to understand what makes ride-sharing fundamentally different from anything else we've built."
Here's the paradigm shift:
URL Shortener, Slack, Instagram, Twitter, WhatsApp, Netflix, YouTube, Dropbox — all had bounded, predictable request patterns. A URL redirect. A message. A feed load. A video play. A file upload. These are IDENTICAL from a systems perspective — they're I/O + compute over a predictable payload.
Ride-sharing is a real-time bipartite matching problem — riders and drivers exist as continuous, GPS-updating streams, and every 30 seconds you're solving "assign N rider requests to M drivers optimally." That's operations research colliding with distributed systems. It's the hardest system-design interview problem by a wide margin.
Uber's answer was to (1) build H3 — a hexagonal geospatial index so every location snaps to a discrete cell, making queries O(1) instead of O(N) (Uber Engineering blog: H3); (2) build DISCO — the Dispatch Optimization Service that does batched bipartite matching every 30s (Uber talk 2016); (3) build DeepETA — a transformer-based ETA predictor with 50% lower error than classical routing (Uber Engineering blog: DeepETA); and (4) build Ringpop — their scalable sharding library for consistent hashing at scale (Uber Engineering blog: Ringpop).
If your interview answer doesn't cite at least H3 and DISCO, you'll fail. Naming DeepETA and Ringpop signals senior-level awareness.
The real 2024 numbers
- ~161M MAPCs (Monthly Active Platform Consumers — Uber's official metric, Q4 2024 earnings)
- ~$162B Gross Bookings 2024 — up 21% YoY
- ~11B trips in 2024
- ~7M active drivers globally
- ~30 second dispatch window — DISCO batched matching cadence
- ~50% reduction in ETA error vs classical routing via DeepETA (Uber Engineering, 2022)
- H3 resolution 9 hexagon (~174m across) — the primary lookup unit for driver-nearest queries
- Multi-region active-active — regional failover in <60s
Interview soundbite: "Uber at 161M MAPCs is a real-time bipartite matching problem, not a request-response problem. Every 30 seconds, DISCO solves 'assign these N rider requests to these M drivers optimally' using H3 hexagonal geo-cells for O(1) proximity queries. DeepETA gives per-trip time-of-arrival predictions with 50% lower error than classical Dijkstra. If it feels like operations research, that's because it is."
The whole journey at a glance
Every 10× in drivers×riders forces a fundamentally different matching strategy:
text═══════════ UBER ARCHITECTURE ACROSS 4 SCALES ═══════════ L4 (10K users) L5 (10M users) L6 (100M users) L7 (161M MAPCs, global) PostGIS + greedy H3 index + DISCO Multi-city + DeepETA Global multi-region 12 weeks · $500/mo 6 months · $50K/mo 18 months · $2M/mo ongoing · $500M/yr ┌────────┐ ┌────────┐ ┌── Regional ALBs ───┐ ┌── 20+ regions ───────┐ │Rider │ │Rider │ │ city-level │ │ anycast + BGP │ │Driver │ │Driver │ │ routing │ │ Uber own edge │ │apps │ │apps │ └─┬──┬──┬──┬─────────┘ └──┬──┬──┬──┬──────────┘ └───┬────┘ └───┬────┘ │ │ │ │ │ │ │ │ │ │ ┌─▼──▼──▼──▼─────┐ │ │ │ │ ┌──▼───┐ ┌──▼──┐ │ Regional API │ ┌───▼──▼──▼──▼──────────┐ │ ALB │ │ ALB │ │ + DISCO svc │ │ Global API Gateway │ │ WSS │ │ WSS │ │ + Trip svc │ │ + Ringpop sharding │ └──┬───┘ └──┬──┘ └───┬──────┬─────┘ │ + Envoy + gRPC │ │ │ │ │ └──┬──┬──┬──┬──────────┘ ┌──▼──────┐ ┌───▼─────────┐ ┌───▼──────▼───┐ │ │ │ │ │ Node.js │ │ Go / Java │ │ DISCO │ ┌──▼──▼──▼──▼──────────┐ │ monolith│ │ 100+ svcs │ │ batched │ │ DISCO (per-city) │ │ + PG │ │ + Ringpop │ │ matching │ │ Global bipartite │ │ + WS │ │ + Kafka │ │ every 30s │ │ optimization │ └──┬──────┘ └───┬─────────┘ └───┬───────────┘ │ + DeepETA │ │ ┌───▼──┐ ┌───▼──────────┐ │ transformer │ │ │Redis │ │Cassandra + │ │ prediction │ │ │GEO │ │Redis GEO │ └──┬──┬──┬──┬─────────┘ │ │+ hot │ │+ H3 index │ │ │ │ │ │ └───┬──┘ └───┬──────────┘ ┌──▼──▼──▼──▼─────────┐ │ │ │ │ Cassandra multi-DC │ ┌──▼───┐ ┌───▼─┐ ┌───▼──────────┐ │ + Redis GEO regions │ │Postgr│ │Postgr│ │Sharded Post- │ │ + Kafka streams for │ │+ Pos-│ │+ shar│ │gres + Schem- │ │ trip events │ │tGIS │ │d by │ │aless (Uber's │ │ + Manifold (S3-like) │ │(driv │ │user │ │KV store) │ └──┬───────────────────┘ │+ trip│ │_id │ └──────────────┘ │ │+ user) │+ RF=2│ ┌──▼───────────────────┐ └──────┘ └──────┘ │ Schemaless (Uber's │ │ MySQL-backed KV │ │ wraps MySQL │ │ ~40 PB per region) │ └───────────────────────┘ ↑ ↑ ↑ ↑ Boring H3 + DISCO enter. Multi-city + DeepETA Global multi-region. PostgreSQL + Batched matching ML routing + Schemaless Ringpop consistent hash. PostGIS + WS. every 30s. MySQL-backed KV. DISCO globally. Ringpop sharding. Chapter 5 Chapters 6+6.5 Chapter 7+7.5 Chapter 8 walks walks through walks through DeepETA walks through 161M through H3 index + DISCO + Schemaless + multi- MAPCs, global topology, L4 MVP batched matching city topology and business economics Key insight: Ride-sharing is a REAL-TIME BIPARTITE MATCHING problem, not a request-response problem. Every 30s, DISCO assigns N riders to M drivers optimally, using H3 hexagons for O(1) geo queries, DeepETA for trip time prediction, Ringpop for consistent-hash sharding. This shape is unique to ride-sharing. Every architecture decision is subordinated to the 30s matching cadence.
The same 4 tiers as clean architecture diagrams
L4 · 10K users · PostGIS + greedy match · $500/mo · 12 weeks:
flowchart TD
RA([Rider app]) -->|WSS| LB[ALB · WebSocket sticky]
DA([Driver app]) -->|WSS every 4s| LB
LB --> API[Node.js monolith<br/>c5.large<br/>~5K concurrent WS]
API --> PG[(Postgres + PostGIS<br/>drivers + riders + trips<br/>+ geo-index)]
API --> ST[Stripe API<br/>payment on trip complete]
classDef n fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
class LB,API,PG,ST nL5 · 10M users · H3 + DISCO batched match · $50K/mo · 6 months:
flowchart TD
RA([Rider app]) -->|WSS| LB[ALB · WSS]
DA([Driver app]) -->|WSS every 4s| LB
LB --> R{Ringpop consistent-hash<br/>by user_id}
R --> A1[API 1]
R --> A2[API 2]
R --> A3[API 3]
A1 --> D[DISCO service<br/>batched match every 30s<br/>H3 index for O(1) geo]
A1 --> RD[(Redis GEO + hot state<br/>current driver locations)]
D --> RD
A1 --> C[(Sharded Postgres by user_id<br/>trips + payments)]
A1 --> KF[Kafka trip events<br/>ML signals · analytics]
classDef n fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef m fill:#fef3c7,stroke:#d97706,color:#78350f
class LB,R,A1,A2,A3 n
class D,RD,C,KF mL6 · 100M users · Multi-city + DeepETA · $2M/mo · 18 months:
flowchart TD
RA([Global rider/driver apps]) -->|WSS + gRPC| LB[Regional ALB · per-city]
LB --> API[Regional API + city-level routing]
API --> D[DISCO per-city<br/>batched match every 30s<br/>H3 index resolution 9<br/>~174m hexagon]
D --> DEEP[DeepETA transformer<br/>predicts trip time · 50% lower error]
DEEP --> API
API --> RGE[(Regional Redis GEO<br/>+ H3 cell state)]
API --> SL[(Schemaless<br/>Uber's MySQL-backed KV<br/>trips + payments)]
API --> KF[Kafka + Flink<br/>trip events + ML signals]
classDef n fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef m fill:#fef3c7,stroke:#d97706,color:#78350f
classDef ml fill:#dcfce7,stroke:#16a34a,color:#14532d
class LB,API n
class RGE,SL,KF m
class D,DEEP mlL7 · 161M MAPCs · Global multi-region + Ringpop · $500M/yr:
flowchart TD
RA([161M MAPCs globally]) -->|WSS + gRPC| CDN[Uber own edge + anycast<br/>20+ regions]
CDN --> LB[Global API Gateway<br/>Envoy + gRPC + Ringpop consistent-hash]
LB --> API[Regional API + city-level routing]
API --> D[DISCO per-city<br/>batched bipartite match every 30s<br/>H3 index for O(1) geo]
API --> DEEP[DeepETA transformer<br/>trip time + surge prediction]
API --> RGE[(Regional Redis GEO + H3 state)]
API --> SL[(Schemaless multi-DC<br/>trips + payments)]
API --> CAS[(Cassandra multi-region<br/>replicated 3x)]
API --> KF[Kafka + Flink streaming<br/>trip events + ML signals + fraud]
API --> MAN[(Manifold<br/>Uber's S3-like blob store)]
classDef n fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef m fill:#fef3c7,stroke:#d97706,color:#78350f
classDef ml fill:#dcfce7,stroke:#16a34a,color:#14532d
class CDN,LB,API n
class RGE,SL,CAS,KF,MAN m
class D,DEEP mlWhy every 10× breaks the architecture
- The geo-index changes. L4 = PostGIS. L5+ = H3 hexagonal cells because H3 gives O(1) proximity queries via hex-neighbor lookup instead of O(N log N) k-nearest-neighbor. This is Uber's biggest engineering contribution to the industry. Reference: Uber H3 blog + open-source.
- Matching changes. L4 = greedy "closest driver". L5+ = batched bipartite optimization. Every 30s, DISCO solves an assignment problem across ALL pending riders and available drivers, minimizing total ETA. Greedy is 10-30% worse. Reference: Uber DISCO talk 2016.
- ETA prediction changes. L4 = "distance ÷ average speed". L6+ = deep-learning model on 100s of features (weather, traffic, time-of-day, driver behavior). DeepETA reduced Uber's ETA error by 50%. Reference: Uber DeepETA blog.
The 3 senior insights before we start Chapter 1
- H3 is Uber's most influential engineering contribution. Every serious geo-based system (Ola, Grab, Lyft, Instacart, DoorDash) uses H3 hexagons or equivalent. Not lat/lng. Not quadtrees. Not R-trees. Hex cells because they have uniform-distance neighbors — 6 neighbors all equidistant, unlike squares. Reference: H3 GitHub.
- DISCO's 30-second batching cadence is the interview signal. Naive answers say "match immediately". Senior answers say "batch every 30s and solve optimally, because greedy matching leaves 10-30% efficiency on the table when driver density is high enough that batching converges."
- Ringpop consistent hashing is Uber's sharding pattern. Not vanilla consistent hashing. Not database sharding. Ringpop is application-level consistent-hash routing + membership + failure detection, all in one library. Reference: Ringpop GitHub.
Chapter map for the journey ahead
- Chapter 1 — Requirements (rider, driver, payment, cancellation, surge)
- Chapter 2 — Capacity estimation (161M MAPCs, 11B trips/year, 30s dispatch)
- Chapter 3 — API design (request ride, driver actions, trip lifecycle)
- Chapter 4 — Data model (users, drivers, trips, payments — with state machines)
- Chapter 4.5 — H3 hexagonal geospatial indexing: how it works + why it wins
- Chapter 5 — L4 MVP: PostGIS + greedy match + WebSocket. Up to 10K users
- Chapter 6 — L5: DISCO batched matching + H3 + Ringpop sharding
- Chapter 6.5 — The batched-matching algorithm: bipartite optimization every 30s
- Chapter 7 — L6: Multi-city topology + DeepETA transformer + Schemaless
- Chapter 7.5 — DeepETA: how a transformer predicts arrival times
- Chapter 8 — L7: 161M MAPCs + global regions + surge + fraud + $500M/yr infra
- Chapter 9 — Failure modes: driver-supply shortage, matching timeout, GPS drift, fraud
- Chapter 10 — Trade-off matrix (Uber vs Lyft vs DoorDash vs Grab)
- Chapter 11 — Interview masterclass: 45-min mock, questions to ask
- Chapter 12 — Defense: the 20 hardest interview questions on Uber
Ready? Chapter 1 next: what did the CTO actually ask for?
Uber at 161M MAPCs is a real-time bipartite matching problem, not a request-response problem. H3 hexagonal geo-index (Uber's biggest open-source contribution) gives O(1) proximity queries. DISCO does batched bipartite matching every 30 seconds. DeepETA transformer predicts trip times with 50% lower error than classical routing. Ringpop is application-level consistent hashing. Naming H3 + DISCO is table stakes; naming DeepETA + Ringpop signals senior-level awareness.
- Why does Uber use H3 hexagons instead of lat/lng or quadtrees?
- What is DISCO and why does it batch every 30 seconds?
- What is DeepETA and how much does it improve ETA accuracy?
- What is Ringpop and how does it differ from vanilla consistent hashing?
- Why is ride-sharing fundamentally a bipartite matching problem?
Every concept below has its own interactive, animated page in the Learning Tracks section. Read them any time you want to go deeper than the mentor prose above — they're the reusable foundation this chapter is built on.
Ringpop is Uber's consistent-hash + membership + failure-detection library. Understanding vanilla consistent hashing is the foundation.
Uber's trip-event stream is the backbone of downstream ML (fraud, ETA, surge) and analytics. Same pattern, different scale.
Chapter 1 next: what did the CTO actually ask for? Ride request, driver assignment, trip state machine, payment, cancellation, surge — each has functional and non-functional requirements. Get these wrong and you'll design the wrong system.
The workload shape — why ride-hailing is NOT a request-response system
Bipartite matching, GPS streams, batched dispatch, and why every other system on this platform got easier the more you understood it
Every other system on this platform — URL Shortener, Instagram, Twitter, Netflix, Kafka, distributed-cache — is a request-response system. A rider taps a button, a server processes the request, a response returns. The request-response mental model is powerful because it composes: you can add caches, add replicas, add regions, and each addition is a pure improvement of the same shape.
Ride-sharing breaks the mental model.
Two independent streams, not a request-response
There are two continuous streams, not a request-response pair:
- Rider request stream: riders tap 'Request Ride' at some rate (~1M requests/day for a metros-scale platform). Each request is a message with a lifespan (the rider is willing to wait a bounded amount of time — say 5 minutes — for a driver).
- Driver GPS stream: every active driver pings their location every ~5 seconds. At Uber scale, that is 7M active drivers × 12 pings/minute = ~1.4M pings/second.
The GPS stream is 4-5 orders of magnitude denser than the request stream. That single fact drives most of the architecture. You cannot process driver-side traffic the same way as rider-side traffic — it must be a stream ingestion (Kafka + Flink), not a request-response API.
Bipartite matching, not point lookup
A URL Shortener's "matching" is: given a short_code, return the long_url. A single-key point lookup. O(1) with a hash index.
Ride-sharing's "matching" is: given N pending rider requests and M available drivers in a geographic region, find an assignment that maximizes some objective function (utilization + rider wait + driver fairness + surge). This is a bipartite matching problem, and the objective function is what makes it interesting.
- Small N, M (say, 5 riders and 10 drivers in one H3 cell): trivial. Brute force.
- Medium N, M (say, 500 riders and 1,000 drivers per H3-parent cell): Kuhn-Munkres (Hungarian algorithm) is O(n³) = ~10^9 operations, tolerable in a 30-second batch window.
- Large N, M (say, all riders and all drivers globally): NP-hard. Impossible to solve exactly at scale.
Uber's DISCO solves this by partitioning by H3 parent cell — each partition solves its own bipartite problem in parallel. You lose ~5% global optimality; you gain ~50× speed. That is the L6 tradeoff conversation the interviewer is looking for.
Time is a first-class dimension
Every URL Shortener redirect is "eternally cacheable" — the mapping from short_code to long_url never changes over the timescale that matters. You can cache it for hours.
Every ride-hailing match is temporally locked. A driver location is valid for ~10 seconds. A rider request is valid for ~5 minutes. Surge multipliers change every 15-30 seconds. The system spends most of its complexity budget managing the fact that its inputs are constantly aging out.
Interview soundbite: "Ride-hailing is what happens when you take bipartite matching, sprinkle it with real-time streams, add surge pricing as a load-shedding mechanism, and multiply by 200 countries. It's operations research plus distributed systems plus economics — every other system-design problem on this platform is easier the more you understand it, but ride-hailing is harder."
The four scale tiers exist because of shape shift, not size
For URL Shortener, the four tiers (L4/L5/L6/L7) are essentially the same architecture with more of everything — more shards, more replicas, more regions. The shape stays constant.
For ride-hailing, the shape changes at each tier:
- L4: request-response. Sync PostGIS. One city, one server.
- L5: streams enter. H3 index, Redis geo-sets, but still request-response for the rider.
- L6: batched matching. DISCO 30-second bipartite. Rider UX shifts to "searching..." state.
- L7: ML in the loop. DeepETA + retention modeling + cross-region orchestration.
Each tier requires a fundamentally different way of thinking, not just more resources. The interviewer is watching for whether you can hold all four architectures in your head simultaneously and articulate the transition triggers.
Clarifying questions
- 1Sync per-request matching or batched DISCO-style dispatch? (Drives latency + utilization tradeoff.)
- 2Global optimality or per-H3-cell partitioned matching? (NP-hard globally; partitioning trades 5% for 50× speed.)
- 3Multi-region active-active or single-region primary? (Drives cost + consistency model.)
- 4Do we adopt H3 from day 1 or start with PostGIS? (Depends on launch-city size.)
- 5Is surge pricing a load-shedding mechanism or purely an economic signal? (Both is honest; volunteering that distinction is the L6 signal.)
Functional
- Rider requests a ride at (lat, long) → system finds the best driver within N km
- Driver GPS ping every 5 s while online → system tracks driver location
- Driver accepts / rejects a match offer within a bounded window (typically 15 s)
- Real-time trip tracking during ride (rider sees driver approach; driver sees route)
- Surge pricing: multiplier per H3 cell based on supply/demand ratio
- Post-trip payment settlement (transactional; not covered here — see Payment System)
Non-functional
- Rider-facing p99 request→offer latency: 30-45 s at scale (DISCO batching is deliberate)
- Driver GPS ingest scales to 5M+ pings/sec globally at Uber scale
- Multi-region active-active — <60 s RTO on regional failover
- Data integrity: no double-issued rides for the same rider (idempotency on ride_id)
- Fairness: match algorithm must respect driver-earnings limits + non-discrimination policies
Ride-hailing is a bipartite matching problem over continuous streams — NOT a request-response problem. Two streams (rider requests, driver GPS pings), one bipartite matching engine, and time as a first-class dimension. Every architectural decision cascades from these three facts.
- Why is ride-hailing NOT a request-response system?
- What is the bipartite matching problem and why is it NP-hard globally?
- Why does H3 exist and what does it solve?
- What is the DISCO 30-second window and why is it a UX tradeoff, not a technical limit?
- Why do the four ride-hailing scale tiers require different mental models, not just more resources?
Every concept below has its own interactive, animated page in the Learning Tracks section. Read them any time you want to go deeper than the mentor prose above — they're the reusable foundation this chapter is built on.
The core algorithmic primitive behind DISCO. Understanding Kuhn-Munkres is what separates L5 candidates from L6.
The GPS ingest stream is a canonical high-throughput write path. Understanding backpressure via Kafka + Flink is the L6+ signal.
Chapter 2 next: back-of-the-envelope. 161M MAPCs at Uber scale, ~1.4M GPS pings/sec, 30-second DISCO batches, per-cell surge windows. The math tells you Kafka partition count, DISCO worker count, and the GPU fleet size for DeepETA.
Components used in this design
Study each component's deep dive to understand it in isolation.
Load Balancer
Distributes incoming traffic across a pool of servers for scale and fault tolerance.
Redis
An in-memory key-value store used for caching, pub/sub, rate limiting, distributed locks, and simple queues.
NoSQL Database
Umbrella for document, wide-column, and key-value stores optimized for horizontal scale over strict schema.
Kafka
A distributed, partitioned, replicated commit log for event streaming, high-throughput ingest, and decoupled services.
Stream Processor (Flink, Kafka Streams, Spark Structured Streaming)
Continuous computation over event streams — windowed aggregations, joins, ML features, and event-driven derived views.
Distributed Lock (ZooKeeper, etcd, Redis Redlock)
Cross-node mutual exclusion — 'only one node can do this at a time' when you can't rely on a single-node lock.
Rate Limiter
Enforces per-caller (per-user, per-IP, per-tenant) request budgets to protect downstream systems from abuse and overload.
Patterns applied in this design
Study each pattern's deep dive for the recurring solution logic.
Cache-aside (Lazy loading)
Reads dominate your workload, and every read hits a slow store (database, disk, network). Latency and load on the primary storage climb until the primary becomes the bottleneck.
Saga (Long-running distributed transaction)
You have a business transaction that spans multiple services or databases (place order → charge payment → reserve inventory → send confirmation). Two-phase commit is too slow, too coupling, and often not available across service boundaries.
Sharding (Horizontal partitioning)
Your dataset or write throughput has outgrown a single node. A single primary DB or single cache node can't hold the data or handle the QPS.
Consistent hashing
You need to shard keys across N nodes, but N changes over time (nodes added, removed, or failed). Naive hash-mod-N reshuffles almost everything on every change — cache is wiped, migration cost is huge.
Explore next — related systems
If you enjoyed this problem, these share similar patterns or challenges.