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