?

Side quest 09Ride-hailing

Uber.

The Real-Time Marketplace Behind 40+ Million Trips Every Day

INTERMEDIATE30 MIN READ6 PHASES · 40 MIN INTERVIEW+120 XP

Mission brief

Design a ride-hailing platform

Quest log0/7 stages · 0/5 checkpoints · 0/7 quick checks
trips a day, rides + deliveries (Q2 2026)
~43M
driver location updates per second at peak (our estimate)
~500K
from ride request to matched driver
< 3 s
between location pings from each online driver
4 s

Company names and logos are trademarks of their owners, shown only to identify each company. This course isn't affiliated with or endorsed by any of them.

watch it first · newThe whole design in a 14-minute animated lecture4 quiz checkpoints · ask the AI about any moment · captions, chapters and speed
Optional · timed run · 6 phases · 40 min

Want it harder? Hide the model answer and work each phase in a scratchpad against its time budget, then reveal and score yourself. Or simply read on: the checkpoints below work either way.

Intro · 2 min

How Do You Match Millions of Riders with Drivers in Under 3 Seconds?

Imagine this challenge: Every second, thousands of people open an app expecting to see available cars near them. Within 3 seconds, you must find the optimal driver from potentially hundreds nearby, calculate the fare, estimate arrival time, and begin the match—all while the driver and rider are both moving.

This is Uber—a real-time, location-based marketplace operating in 15,000+ cities across 70+ countries. It's not just a taxi app. It's a masterclass in geospatial indexing, dynamic pricing, and building systems that make split-second decisions at global scale.

Uber matching · the rider's H3 cell, then rings k=1 and k=2 searched until the nearest driver is dispatched
LIVEUber matching · the rider's H3 cell, then rings k=1 and k=2 searched until the nearest driver is dispatched
The Uber scale (as of 2026)
Marketplace
  • Monthly Active Platform Consumers: 208 Million (Q2 2026)
  • Drivers and couriers: ~10 Million monthly active (2026)
  • Daily Trips: ~43 Million (rides + deliveries, Q2 2026)
  • Cities: 15,000+
  • Countries: 70+
Real-time (this design's targets and estimates)
  • Location updates/second: ~500K at peak (estimated below)
  • Match latency target: < 3 seconds
  • ETA accuracy target: ~95% within 3 minutes
  • Driver ping response: < 15 seconds
Data
  • Events processed/day: Hundreds of billions
  • Data pipelines: 200,000+
  • ML models in production: Thousands
  • Kafka messages/day: Trillions
Infrastructure
  • Microservices: Thousands
  • Data centers: Multiple (hybrid cloud)
  • Engineers: Thousands (of ~34,000 employees, end of 2025)
Business (2025)
  • Gross Bookings: $193.5 Billion
  • Revenue: $52.0 Billion
  • Trips completed: ~13.6 Billion (in 2025 alone)

This is the system we'll design today—and discover how Uber matches riders with drivers faster than you can blink.

ExplainerThe whole quest: Uber

Uber runs ~43 million trips a day and must match a rider in under 3 seconds: 1.5 million online drivers ping every 4 s (375,000 updates a second) against ~1,000 trip requests. Pings land in an in-memory H3 index, Pricing quotes an upfront fare, Matching scores nearby drivers and offers the ride for 15 s, and the trip is created exactly once. The deep dives cover H3 rings, surge with smoothing, weighted scoring and ETAs, then city sharding, silent drivers and GPS drift, a regional failover and idempotent trip completion.

The Interview Begins#

You walk into the interview room. The interviewer smiles and gestures to the whiteboard.

Interviewer

"Thanks for coming in. Today we're going to design a ride-hailing platform like Uber. I'm interested in how you think about real-time matching, geospatial systems, and dynamic pricing. Please think out loud—this is collaborative."

They write on the whiteboard:

Design a Ride-Hailing Platform
Requirements
  • Match riders with nearby drivers in real-time (< 3 seconds)
  • Track driver locations continuously
  • Calculate accurate ETAs and fares
  • Implement dynamic (surge) pricing
  • Support multiple ride types (UberX, Pool, Black, etc.)
  • Handle global scale across thousands of cities
Interviewer

"Take a few minutes to think about this, then walk me through your approach. We have about 45 minutes."

Phase 1 · Stage 1 of 7 · 5 min budget · 1 min read

Phase 1: Requirements Clarification (5 minutes)#

Before diving in, you take a breath and start asking questions.

Your Questions#

Transcript
You

"Before I start designing, let me clarify some requirements. What's our target scale—how many concurrent users and trips?"

Interviewer

"Think global scale. Assume 150+ million monthly riders, 7+ million drivers and 30 million ride trips per day."

You

"For the matching process, what's the acceptable latency? How fast should a rider see available cars?"

Interviewer

"Under 3 seconds from request to match. Users should see nearby drivers update in real-time as they move."

You

"How frequently do drivers report their location?"

Interviewer

"Every 4 seconds while online. That's hundreds of thousands of location updates per second globally."

You

"What about the ETA calculation? How accurate does it need to be?"

Interviewer

"Within 3 minutes of actual arrival time, 95% of the time. This is critical for user trust."

You

"Perfect. Let me summarize the requirements."

Functional Requirements#

Functional requirements
1. Rider experience
  • See nearby available drivers on map
  • Request a ride with pickup and destination
  • Get fare estimate before confirming
  • Track driver en route to pickup
  • Track trip progress to destination
  • Rate driver after trip
2. Driver experience
  • Go online/offline
  • Receive ride requests with accept/decline
  • Navigate to pickup and destination
  • See earnings and trip history
  • Receive surge notifications
3. Matching
  • Find optimal driver for rider request
  • Consider distance, ETA, driver rating
  • Support ride types (X, Pool, Black, XL)
  • Handle driver cancellations gracefully
4. Pricing
  • Calculate base fare (time + distance)
  • Apply dynamic surge pricing
  • Show upfront pricing to riders
  • Split fares for Pool rides

Non-Functional Requirements#

Non-functional requirements
1. Latency
  • Match request to driver assignment: < 3 seconds
  • Location update processing: < 100ms
  • ETA calculation: < 500ms
  • Fare calculation: < 200ms
2. Availability
  • 99.99% for ride requests
  • Graceful degradation (show cached data if needed)
  • Multi-region failover
3. Scale
  • 30 million trips/day
  • Hundreds of thousands of location updates/second
  • Peak handling (New Year's Eve, major events)
4. Consistency
  • Strong consistency for trip state
  • Eventual consistency for driver locations
  • Exactly-once trip creation

Phase 2 · Stage 2 of 7 · 5 min budget · 2 min read

Phase 2: Back of the Envelope Estimation (5 minutes)#

You

"Let me work through the numbers to understand the scale."

Your turn · checkpoint 1not answered

Before the numbers: which write rate dominates?

Assume 7.4 million ride drivers, about 20% online at once, each sending its location every 4 seconds. Assume riders take 30 million trips a day, with a 3× evening peak. Work out location updates per second and trip requests per second. Which one shapes the storage design?

Think it through, or jot it down. Then compare.

One strong answer

  • Locations: 7.4M × 20% ≈ 1.5M online; 1.5M ÷ 4 s = 375,000 updates/s, around 500,000 at peak.
  • Trips: 30M × 3 ÷ 86,400 s ≈ 1,000 requests/s at peak.
  • Locations win by roughly 500 to 1. They need an in-memory store that simply overwrites each driver's latest position and tolerates a few seconds of staleness. Trips are few but must be strongly consistent and created exactly once, so they belong in a durable, transactional store.

Traffic Estimation#

WorksheetTraffic estimation
  • Ride requests
    • Daily ride trips (assumed; Uber's ~43M/day also counts deliveries): 30 million
    • Peak multiplier: 3x (evening rush, events)
    • Peak trips/hour: 30M × 3 / 24 = 3.75 million
    • Peak trips/second: ~1,000 trips/second
  • Location updates
    • Active ride drivers (assumed; Uber's ~10M also counts couriers): 7.4 million
    • Online at any time: ~20% = 1.5 million
    • Update frequency: Every 4 seconds
    • Location updates/second: 1.5M / 4 = 375,000/second
    • Peak (~1.33x, a third above the baseline): ~500,000 updates/second

Storage Estimation#

WorksheetStorage estimation
  • Trip data
    • Trips per day: 30 million
    • Trip record size: ~5 KB (locations, timestamps, fare)
    • Daily trip storage: 30M × 5 KB = 150 GB/day
    • Yearly: ~55 TB/year
  • Location history
    • Updates per second: 500,000
    • Bytes per update: ~100 bytes (lat, lng, timestamp, driver_id)
    • Daily location data: 500K × 100 × 86,400 = 4.3 TB/day
    • Retention (7 days): ~30 TB

Key Metrics Summary#

Estimation summary
Traffic
  • Peak trip requests: 1,000/second
  • Location updates: 500,000/second
  • API requests (total): ~5 million/second
Storage
  • Trip data: 150 GB/day
  • Location data: 4.3 TB/day
  • Total (with retention): ~100 TB active
Compute
  • Matching decisions: 1,000/second
  • ETA calculations: ~50,000/second (up to 50 candidates scored per match, at 1,000 matches a second)
  • Surge calculations: Per cell, every 30 seconds
Quick check3 questions · +5 XP each

Requirements and estimation

  1. Q1/3 Using the lesson's numbers (~1.5M drivers online), product wants driver pings every 2 seconds instead of 4. What baseline load should the Location Service plan for?

    Answer & explanation

    Answer: B

    1.5M drivers / 2 s = 750,000 updates/s, double today's 375,000/s baseline (and ~1M/s at the same peak, a third higher). Halving the interval doubles the write rate; it does not change the number of drivers online.

  2. Q2/3 Why does the design demand strong consistency for trip state but accept eventual consistency for driver locations?

    Answer & explanation

    Answer: C

    A slightly stale dot on the map self-corrects at the next ping. Trip creation must be exactly-once: two trips for one request means double charges or two drivers sent.

  3. Q3/3 Which data stream drives the storage design, and how does the estimate handle it?

    Answer & explanation

    Answer: D

    Location pings are roughly 30x the trip data by volume. A short retention window (~30 TB for 7 days) keeps that stream bounded.

Phase 3 · Stage 3 of 7 · 10 min budget · 2 min read

Phase 3: High-Level Design (10 minutes)#

You

"Now let me sketch out the high-level architecture."

Your turn · checkpoint 2not answered

Sketch it first: one ride request, end to end

A rider taps Request. Which services take part, in what order, and which of their data needs strong consistency versus "fresh within a few seconds"?

Think it through, or jot it down. Then compare.

One strong answer

  • Location service: ingests driver pings and keeps an in-memory geospatial index of available drivers. Eventually consistent; a few seconds stale is fine.
  • Pricing: quotes an upfront fare using the surge for the pickup cell.
  • Dispatch / matching: finds candidates near the pickup from the index, scores them with ETAs, and offers the trip to the best driver, waiting up to 15 seconds before moving to the next.
  • Trip service: when a driver accepts, creates the trip exactly once and runs its state machine (matched, en route, on trip, completed). Strongly consistent.
  • Events: every state change is published to Kafka for analytics, ETAs and surge.

System Architecture#

DiagramUber high-level architecture
%% title: Uber high-level architecture
flowchart TD
  subgraph CL["Clients"]
    RA@{ icon: "sd:phone", label: "Rider App" }
    DA@{ icon: "sd:car", label: "Driver App" }
  end
  GW["API GATEWAY<br/>Rate Limiting · Auth · Routing · Load Balancing"]:::edge
  RA ==>|"WebSocket + HTTP"| GW
  DA ==>|"WebSocket + HTTP"| GW
  subgraph US["Uber services"]
    direction TB
    subgraph RT["Real-time layer"]
      direction TB
      WS["WebSocket Manager"]:::service
      LS["Location Service"]:::hot
      PS["Push Service"]:::service
    end
    subgraph CS["Core services"]
      direction TB
      MS["Matching Service"]:::core
      TS["Trip Service"]:::core
      PR["Pricing Service"]:::core
      ET["ETA Service"]:::core
      MP["Map Service"]:::core
      PY["Payment Service"]:::core
      MS ~~~ ET
      TS ~~~ MP
      PR ~~~ PY
    end
    subgraph DL["Data layer"]
      direction TB
      SP@{ icon: "tech:spanner", label: "Spanner (Trips)" }
      RD@{ icon: "tech:redis", label: "Redis (Locations)" }
      CA@{ icon: "tech:cassandra", label: "Cassandra (History)" }
      KF@{ icon: "tech:kafka", label: "Kafka (Events)" }
    end
    H3@{ icon: "sd:hexagons", label: "GEOSPATIAL INDEX<br/>H3 Hexagonal Grid (Driver Location Index)" }
    RT ~~~ CS
    CS ~~~ DL
    DL ~~~ H3
  end
  GW ==> RT
  class RA,DA client
  class SP,CA,H3 store
  class RD cache
  class KF queue

Data Flow: Requesting a Ride#

DiagramRide request flow
%% title: Ride request flow
sequenceDiagram
  autonumber
  participant R as Rider app
  participant G as API Gateway
  participant L as Location Service
  participant M as Matching Service
  participant D as Driver app
  participant T as Trip Service
  participant P as Pricing Service
  R->>G: RIDER OPENS APP · WebSocket connection established
  L-->>R: See nearby drivers (from Location Service)
  R->>G: RIDER REQUESTS RIDE · POST /ride-request {pickup, destination, ride_type}
  G->>M: API Gateway → Matching Service
  M->>L: Query Location Service for nearby drivers
  L-->>M: nearby drivers
  Note over M: Filter by ride type, rating, availability<br/>Calculate ETA for each candidate<br/>Score and rank candidates<br/>Select optimal driver
  M->>D: DRIVER NOTIFICATION · push via WebSocket
  Note over D: Driver has 15 seconds to accept
  opt IF ACCEPTED
    D-->>M: accept
    M->>T: Trip Service creates trip record
    M->>P: Pricing Service calculates fare
    T-->>R: trip details
    T-->>D: trip details
    Note over R,D: Navigation begins
  end
  loop DURING TRIP · every 4 seconds
    D-)L: driver location update
    L-)R: real-time progress (ETA recalculated continuously)
  end
  Note over T,P: TRIP COMPLETION<br/>Calculate final fare · Process payment<br/>Request ratings · Update driver availability
  Note over R,P: TOTAL LATENCY: < 3 seconds (request to driver notification)
Quick check3 questions · +5 XP each

High-level design

  1. Q1/3 A ride request reaches the chosen driver's phone 2 seconds after the rider taps "Request", and the driver accepts 10 seconds later. Was the < 3 s target missed?

    Answer & explanation

    Answer: A

    The flow explicitly measures total latency from request to driver notification. Human accept time is bounded separately by the 15 s timeout.

  2. Q2/3 Where should each online driver's current position live, given ~500,000 updates/second at peak?

    Answer & explanation

    Answer: D

    Current positions are hot, overwritten every 4 seconds and read on every match: an in-memory store fits. Spanner's strong consistency is reserved for trips; Cassandra holds history, not the live index.

  3. Q3/3 Why do both apps keep a WebSocket open instead of polling the API?

    Answer & explanation

    Answer: B

    Dispatch is server-initiated: a driver must get a request within seconds, and riders see positions every 4 seconds. Polling either lags or floods the gateway with empty responses.

Phase 4 · Stage 4 of 7 · 20 min budget · 17 min read

Phase 4: Deep Dives (20 minutes)#

Interviewer

"Let's dive deeper. Tell me about the geospatial indexing system."

Your turn · checkpoint 3not answered

Who is near me?

1.5 million drivers are moving all the time. How would you answer "which available drivers are within about 2 km of this rider?" in under 100 ms without checking every driver?

Think it through, or jot it down. Then compare.

One strong answer

  • Bucket the map: turn every location into a cell ID (an H3 hexagon at about street-block size) and keep cell → set of available drivers in memory, updated on every ping. A driver only moves between sets when they cross into a new cell.
  • Search rings: look in the rider's cell, then the ring of neighbouring cells around it (k = 1, then k = 2…) until there are enough candidates.
  • Then be precise: compute real distance and ETA only for those few dozen candidates.
  • Why hexagons: every neighbour is the same distance from the centre, so rings of cells approximate circles well.
  • Shard by geography: a city's index lives near that city.

Deep Dive 1: H3 Geospatial Indexing (Week 1 Concepts)#

The Problem#

Geospatial challenge
Finding nearby drivers
  • "Find all drivers within 2km of this location"
  • Naive approach: Check distance to ALL drivers
  • With 1.5M online drivers: Too slow
Requirements
  • Sub-100ms lookups
  • Works globally (not just one city)
  • Handles drivers that are constantly moving
  • Efficient for both sparse and dense areas

The Solution: H3 Hexagonal Grid#

H3 hexagonal grid system
Why hexagons?
  • Squares: 2 different neighbor distances (edge vs corner)
  • Triangles: 3 different neighbor distances
  • Hexagons: 1 uniform distance to ALL neighbors (ok)

A hexagon A surrounded by its six neighbors B, C, D, E, F, G: all neighbors (B,C,D,E,F,G) are equidistant from A.

H3 resolution levels
Resolution Cell area Scale
0 ~4.36 million km² continent scale
7 ~5.16 km² city district
9 ~0.105 km² (~105,000 m², edge ~200 m) street block ← a common choice for matching
15 ~0.9 m² precise location
Hierarchical structure

Each hexagon subdivides into ~7 child hexagons. Parent cells contain children (useful for aggregation).

  • Resolution 7 (city)
    • Resolution 8 (neighborhood)
      • Resolution 9 (street block)
        • Resolution 10 (building level)
ExplainerH3 cells, ring search and dispatch

Every 4-second ping maps a driver to one resolution-9 H3 cell and moves it between cell sets only when it crosses a cell edge; its driver hash expires 30 s after pings stop. A request reads the rider's cell, then rings k = 1 and k = 2, skips busy drivers and sorts the rest by real distance. Candidates are scored (0.4 ETA, 0.2 rating, 0.2 acceptance, 0.2 wait), the best is offered the ride for 15 s, then the next.

Implementation#

pythonDriverLocation · 157 lines
# geospatial/h3_location_index.py

"""
H3-Based Location Index for Uber

Uses Uber's open-source H3 hexagonal grid system for
efficient geospatial queries.
"""

from dataclasses import dataclass
from typing import List, Set, Optional
import h3
import redis


@dataclass
class DriverLocation:
    driver_id: str
    latitude: float
    longitude: float
    timestamp: float
    ride_types: List[str]
    rating: float
    is_available: bool


class H3LocationIndex:
    """
    Location index using H3 hexagonal grid.
    
    Key insight: Convert 2D lat/lng to 1D H3 cell ID
    for O(1) lookups instead of O(n) distance calculations.
    
    Applies concepts:
    - Week 1: Partitioning strategies
    - Week 4: Caching for low-latency lookups
    """
    
    # Resolution 9: ~0.1 km² hexagons (edge ~200 m), good for urban areas
    DEFAULT_RESOLUTION = 9
    
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client
        self.resolution = self.DEFAULT_RESOLUTION
    
    def update_driver_location(self, driver: DriverLocation) -> None:
        """
        Update driver location in the index.
        
        Called every 4 seconds per driver.
        Must be extremely fast (< 10ms).
        """
        # Convert lat/lng to H3 cell
        cell_id = h3.latlng_to_cell(
            driver.latitude, 
            driver.longitude, 
            self.resolution
        )
        
        # Remove from old cell (if moved)
        old_cell = self.redis.hget(f"driver:{driver.driver_id}", "cell")
        if old_cell and old_cell != cell_id:
            self.redis.srem(f"cell:{old_cell}", driver.driver_id)
        
        # Add to new cell
        self.redis.sadd(f"cell:{cell_id}", driver.driver_id)
        
        # Store driver details
        self.redis.hset(f"driver:{driver.driver_id}", mapping={
            "cell": cell_id,
            "lat": driver.latitude,
            "lng": driver.longitude,
            "ts": driver.timestamp,
            "available": int(driver.is_available),
            "rating": driver.rating
        })
        
        # Set TTL for automatic cleanup if driver goes offline
        self.redis.expire(f"driver:{driver.driver_id}", 30)
    
    def find_nearby_drivers(
        self, 
        latitude: float, 
        longitude: float,
        radius_km: float = 2.0,
        ride_type: str = "UberX",
        limit: int = 10
    ) -> List[DriverLocation]:
        """
        Find available drivers near a location.
        
        Uses H3 k-ring to get nearby cells, then fetches
        drivers from those cells.
        """
        # Get center cell
        center_cell = h3.latlng_to_cell(
            latitude, longitude, self.resolution
        )
        
        # Widen ring by ring (k = 1, then 2, ...) until there are enough
        # candidates, up to the radius. At resolution 9 the edge is ~200 m,
        # so adjacent cell centres are ~0.35 km apart (sqrt(3) x edge):
        # 2 km allows up to k = 6
        max_k = int(radius_km / 0.35) + 1
        
        candidates = []
        searched = set()
        for k in range(1, max_k + 1):
            # Only the cells this ring adds (grid_disk includes the centre)
            ring_cells = set(h3.grid_disk(center_cell, k)) - searched
            searched |= ring_cells
            for cell_id in ring_cells:
                driver_ids = self.redis.smembers(f"cell:{cell_id}")
                for driver_id in driver_ids:
                    driver_data = self.redis.hgetall(f"driver:{driver_id}")
                    # Hash values come back as strings: "0" is truthy, so
                    # compare explicitly (decode_responses=True assumed)
                    if driver_data and driver_data.get("available") == "1":
                        candidates.append(self._to_driver_location(
                            driver_id, driver_data
                        ))
            if len(candidates) >= limit:
                break   # enough drivers: stop widening
        
        # Sort by distance and return top matches
        candidates.sort(
            key=lambda d: self._haversine_distance(
                latitude, longitude, d.latitude, d.longitude
            )
        )
        
        return candidates[:limit]
    
    def get_drivers_in_cell(self, cell_id: str) -> Set[str]:
        """Get all driver IDs in a specific H3 cell."""
        return self.redis.smembers(f"cell:{cell_id}")
    
    def _haversine_distance(
        self, lat1: float, lng1: float, 
        lat2: float, lng2: float
    ) -> float:
        """Calculate distance between two points in km."""
        from math import radians, sin, cos, sqrt, atan2
        
        R = 6371  # Earth's radius in km
        
        lat1, lng1, lat2, lng2 = map(
            radians, [lat1, lng1, lat2, lng2]
        )
        
        dlat = lat2 - lat1
        dlng = lng2 - lng1
        
        a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlng/2)**2
        c = 2 * atan2(sqrt(a), sqrt(1-a))
        
        return R * c

Deep Dive 2: Dynamic Surge Pricing (Week 5 Concepts)#

Interviewer

"How does surge pricing work?"

The Problem#

Supply-demand imbalance

Scenario: Friday night, 11 PM, downtown

  • Rider requests: 500/minute
  • Available drivers: 50
  • Result: 10-minute wait times, unhappy users
Without surge pricing
  • Drivers don't know demand is high
  • No incentive to drive to busy areas
  • Riders wait forever or can't get a ride
With surge pricing
  • Higher prices attract more drivers
  • Some riders defer (reducing demand)
  • Market reaches equilibrium faster

The Solution: Real-Time Surge Calculation#

Surge pricing algorithm
Input signals
  • Ride requests in H3 cell (last 5 minutes)
  • Available drivers in cell + neighbors
  • Historical demand patterns (time, day, events)
  • Current trip completion rate
  • Weather data
Surge calculation

demand_supply_ratio = requests / available_drivers

Ratio Surge
< 1.0 1.0 (no surge)
= 2.0 ≈ 1.5x
= 4.0 ≈ 2.5x
> 10.0 cap (e.g., 5.0x)
Smoothing
  • Don't change surge too rapidly (confusing for users)
  • Use exponential moving average
  • Gradually ramp up and down
Visualization

City map with H3 cells colored by surge (rows of the hex grid, left to right):

Row Cell surge values
1 1.0 · 1.2 · 1.0
2 1.0 · 2.5 · 1.5
3 1.2 · 3.0 · 1.2
4 1.0 · 2.0 · 1.5
5 1.0 · 1.2 · 1.0

Legend: 1.0x = No surge (gray) · 1.5x = Light surge (yellow) · 2.5x = Medium surge (orange) · 3.0x = High surge (red)

Implementation#

pythonSurgeConfig · 190 lines
# pricing/surge_service.py

"""
Dynamic Surge Pricing Service

Calculates real-time surge multipliers based on
supply-demand imbalance in each H3 cell.
"""

from dataclasses import dataclass
from typing import Dict, Optional
from datetime import datetime, timedelta
import h3


@dataclass
class SurgeConfig:
    min_surge: float = 1.0
    max_surge: float = 5.0
    update_interval_seconds: int = 30
    smoothing_factor: float = 0.3  # For exponential moving average
    demand_window_minutes: int = 5


@dataclass
class CellSurge:
    cell_id: str
    surge_multiplier: float
    demand_count: int
    supply_count: int
    updated_at: datetime


class SurgePricingService:
    """
    Calculate surge pricing per H3 cell.
    
    Key principles:
    - Higher demand → higher surge
    - More drivers → lower surge
    - Smooth transitions (no sudden jumps)
    - Respect max cap (emergencies, PR concerns)
    
    Applies concepts:
    - Week 5: Eventual consistency (surge propagates)
    - Week 8: Real-time analytics pipeline
    """
    
    def __init__(
        self, 
        config: SurgeConfig,
        location_index,
        demand_tracker,
        cache
    ):
        self.config = config
        self.location_index = location_index
        self.demand_tracker = demand_tracker
        self.cache = cache
        
        # Surge curve parameters (tuned via ML)
        self.surge_curve = [
            (1.0, 1.0),   # ratio 1.0 → surge 1.0
            (1.5, 1.25),  # ratio 1.5 → surge 1.25
            (2.0, 1.5),   # ratio 2.0 → surge 1.5
            (3.0, 2.0),   # ratio 3.0 → surge 2.0
            (5.0, 3.0),   # ratio 5.0 → surge 3.0
            (10.0, 5.0),  # ratio 10.0 → surge 5.0 (cap)
        ]
    
    def get_surge_for_location(
        self, 
        latitude: float, 
        longitude: float
    ) -> float:
        """Get current surge multiplier for a location."""
        cell_id = h3.latlng_to_cell(latitude, longitude, 9)
        
        # Check cache first
        cached = self.cache.get(f"surge:{cell_id}")
        if cached:
            return float(cached)
        
        # Calculate fresh surge
        surge = self._calculate_cell_surge(cell_id)
        
        # Cache for 30 seconds
        self.cache.setex(f"surge:{cell_id}", 30, surge)
        
        return surge
    
    def _calculate_cell_surge(self, cell_id: str) -> float:
        """Calculate surge for a specific H3 cell."""
        # Get demand (requests in last 5 minutes)
        demand = self.demand_tracker.get_request_count(
            cell_id, 
            window_minutes=self.config.demand_window_minutes
        )
        
        # Get supply (available drivers in cell + neighbors)
        nearby_cells = h3.grid_disk(cell_id, 1)  # Cell + 6 neighbors
        supply = sum(
            len(self.location_index.get_drivers_in_cell(c))
            for c in nearby_cells
        )
        
        # Prevent division by zero
        if supply == 0:
            return self.config.max_surge if demand > 0 else self.config.min_surge
        
        # Calculate demand/supply ratio
        ratio = demand / supply
        
        # Map ratio to surge using curve
        raw_surge = self._ratio_to_surge(ratio)
        
        # Apply smoothing (don't change too fast). The 30 s "surge:"
        # entry has already expired when we get here, so keep the last
        # published value under a longer-lived key.
        previous_surge = self.cache.get(f"surge_last:{cell_id}")
        if previous_surge:
            raw_surge = self._smooth_surge(
                float(previous_surge), 
                raw_surge
            )
        
        # Clamp to bounds
        surge = max(
            self.config.min_surge,
            min(self.config.max_surge, raw_surge)
        )
        self.cache.setex(f"surge_last:{cell_id}", 3600, surge)
        return surge
    
    def _ratio_to_surge(self, ratio: float) -> float:
        """Convert demand/supply ratio to surge multiplier."""
        # Below the first point (ratio < 1.0): no surge
        if ratio <= self.surge_curve[0][0]:
            return self.surge_curve[0][1]
        
        # Linear interpolation between curve points
        for i in range(len(self.surge_curve) - 1):
            r1, s1 = self.surge_curve[i]
            r2, s2 = self.surge_curve[i + 1]
            
            if r1 <= ratio <= r2:
                # Linear interpolation
                t = (ratio - r1) / (r2 - r1)
                return s1 + t * (s2 - s1)
        
        # Above max ratio
        return self.surge_curve[-1][1]
    
    def _smooth_surge(
        self, 
        previous: float, 
        current: float
    ) -> float:
        """Apply exponential moving average for smooth transitions."""
        alpha = self.config.smoothing_factor
        return alpha * current + (1 - alpha) * previous
    
    def calculate_fare(
        self,
        distance_km: float,
        duration_minutes: float,
        surge: float,
        ride_type: str = "UberX"
    ) -> float:
        """Calculate total fare with surge."""
        # Base rates (vary by city/ride type)
        rates = {
            "UberX": {"base": 2.50, "per_km": 1.20, "per_min": 0.25},
            "UberXL": {"base": 4.00, "per_km": 1.80, "per_min": 0.35},
            "UberBlack": {"base": 8.00, "per_km": 3.00, "per_min": 0.50},
        }
        
        rate = rates.get(ride_type, rates["UberX"])
        
        base_fare = (
            rate["base"] + 
            rate["per_km"] * distance_km + 
            rate["per_min"] * duration_minutes
        )
        
        # Apply surge
        surged_fare = base_fare * surge
        
        # Minimum fare
        return max(surged_fare, 5.00)
Quick check3 questions · +5 XP each

H3 indexing and surge pricing

  1. Q1/3 Why does Uber index drivers on hexagons rather than a square grid?

    Answer & explanation

    Answer: D

    Squares have edge and corner neighbors at two different distances, which distorts "within X km" searches. The code still sorts candidates by haversine distance after gathering them from the k-ring.

  2. Q2/3 A cell's previous surge was 1.0x and the raw demand/supply curve now says 3.0x. With the lesson's EMA (alpha = 0.3), what surge is published?

    Answer & explanation

    Answer: B

    EMA = 0.3 x 3.0 + 0.7 x 1.0 = 1.6x. Smoothing ramps surge gradually so prices do not jump in one step and confuse riders.

  3. Q3/3 Surge counts supply as drivers in the cell plus its 6 neighbors, not just the cell itself. What would go wrong with the cell alone?

    Answer & explanation

    Answer: C

    Resolution-9 hexagons are only a few hundred metres across; nearby drivers frequently sit in adjacent cells. Counting only the center cell undercounts supply and inflates the demand/supply ratio.

Deep Dive 3: Matching Algorithm (Week 1 Concepts)#

Interviewer

"How do you match riders with the optimal driver?"

Your turn · checkpoint 4not answered

The nearest driver isn't always the best one

Why can "send the request to the closest driver" be a poor policy? What would you do in a busy downtown at rush hour?

Think it through, or jot it down. Then compare.

One strong answer

  • Distance isn't time: the closest car by straight line may be across a river or on a one-way street; rank by ETA.
  • Other factors matter: the right ride type, rating, acceptance rate, and fairness to drivers who have waited longest.
  • Offer and fall back: offer to the best-scoring driver, wait up to 15 seconds, then move to the next.
  • Batch when it's busy: greedy one-at-a-time choices give one rider the nearest car and leave the next with a far one. Collect requests over a short window (say 2 seconds) and solve the assignment for all of them together to minimise total pickup time.

The Problem#

Matching challenge
Given
  • 1 rider requesting a ride
  • 50 nearby available drivers
Find the "best" match considering
  • Distance (affects ETA and driver cost)
  • Driver rating
  • Driver's acceptance rate
  • Ride type compatibility
  • Fairness (drivers waiting longer get priority)

The Solution: Weighted Scoring with Dispatch#

Matching algorithm
Step 1: Candidate selection
  • Query H3 index for drivers within radius
  • Filter: available, correct ride type, not already pinged
Step 2: Scoring

For each candidate, calculate:

text4 lines
score = w1 × ETA_score +
        w2 × rating_score +
        w3 × acceptance_score +
        w4 × wait_time_score

Where:

  • ETA_score = 1 - (ETA / max_ETA) — closer is better
  • rating_score = (rating - 4.0) / 1.0 — higher is better
  • acceptance_score = acceptance_rate — higher is better
  • wait_time_score = min(wait_time_minutes / 30, 1) — longer wait = priority (the code uses seconds: wait_time_seconds / 1800)
Step 3: Dispatch
  • Send request to highest-scoring driver
  • Wait up to 15 seconds for response
  • If declined/timeout → next best driver
Optimization: batch matching

For high-volume periods:

  • Collect requests for 2-second windows
  • Solve assignment problem (Hungarian algorithm)
  • Globally optimize rider-driver pairs

Implementation#

pythonMatchResult · 211 lines
# matching/dispatch_service.py

"""
Rider-Driver Matching Service

Finds optimal driver for each ride request using
weighted scoring and ETA estimation.
"""

from dataclasses import dataclass
from typing import List, Optional, Tuple
from enum import Enum
import asyncio


class MatchResult(Enum):
    MATCHED = "matched"
    NO_DRIVERS = "no_drivers"
    ALL_DECLINED = "all_declined"
    TIMEOUT = "timeout"


@dataclass
class MatchRequest:
    request_id: str
    rider_id: str
    pickup_lat: float
    pickup_lng: float
    destination_lat: float
    destination_lng: float
    ride_type: str


@dataclass
class DriverCandidate:
    driver_id: str
    latitude: float
    longitude: float
    rating: float
    acceptance_rate: float
    wait_time_seconds: int
    eta_seconds: int
    score: float = 0.0


class MatchingService:
    """
    Match riders with optimal drivers.
    
    Uses weighted scoring to balance multiple factors:
    - ETA (user experience)
    - Driver rating (quality)
    - Acceptance rate (reliability)
    - Wait time (fairness to drivers)
    
    Applies concepts:
    - Week 1: Partitioning (H3-based driver lookup)
    - Week 2: Timeouts and retries
    """
    
    # Scoring weights (tuned via ML/experimentation)
    WEIGHTS = {
        "eta": 0.4,
        "rating": 0.2,
        "acceptance": 0.2,
        "wait_time": 0.2
    }
    
    DRIVER_RESPONSE_TIMEOUT = 15  # seconds
    MAX_CANDIDATES = 10
    
    def __init__(
        self, 
        location_index,
        eta_service,
        driver_service,
        notification_service
    ):
        self.location_index = location_index
        self.eta_service = eta_service
        self.driver_service = driver_service
        self.notifications = notification_service
    
    async def find_match(
        self, 
        request: MatchRequest
    ) -> Tuple[MatchResult, Optional[str]]:
        """
        Find and dispatch to optimal driver.
        
        Returns (result, driver_id or None)
        """
        # Step 1: Find nearby candidates
        candidates = await self._get_candidates(request)
        
        if not candidates:
            return (MatchResult.NO_DRIVERS, None)
        
        # Step 2: Score and rank candidates
        scored = await self._score_candidates(request, candidates)
        scored.sort(key=lambda c: c.score, reverse=True)
        
        # Step 3: Dispatch to drivers in order
        for candidate in scored[:self.MAX_CANDIDATES]:
            accepted = await self._dispatch_to_driver(
                request, candidate
            )
            if accepted:
                return (MatchResult.MATCHED, candidate.driver_id)
        
        return (MatchResult.ALL_DECLINED, None)
    
    async def _get_candidates(
        self, 
        request: MatchRequest
    ) -> List[DriverCandidate]:
        """Get available drivers near pickup location."""
        drivers = self.location_index.find_nearby_drivers(
            request.pickup_lat,
            request.pickup_lng,
            radius_km=3.0,
            ride_type=request.ride_type,
            limit=50
        )
        
        candidates = []
        for driver in drivers:
            if not driver.is_available:
                continue
            
            # Get driver stats
            stats = await self.driver_service.get_stats(driver.driver_id)
            
            # Calculate ETA
            eta = await self.eta_service.calculate(
                driver.latitude, driver.longitude,
                request.pickup_lat, request.pickup_lng
            )
            
            candidates.append(DriverCandidate(
                driver_id=driver.driver_id,
                latitude=driver.latitude,
                longitude=driver.longitude,
                rating=driver.rating,
                acceptance_rate=stats.acceptance_rate,
                wait_time_seconds=stats.wait_time_seconds,
                eta_seconds=eta
            ))
        
        return candidates
    
    async def _score_candidates(
        self,
        request: MatchRequest,
        candidates: List[DriverCandidate]
    ) -> List[DriverCandidate]:
        """Calculate match score for each candidate."""
        max_eta = max(c.eta_seconds for c in candidates) or 1
        
        for candidate in candidates:
            # Normalize scores to 0-1 range
            eta_score = 1 - (candidate.eta_seconds / max_eta)
            rating_score = (candidate.rating - 4.0) / 1.0
            acceptance_score = candidate.acceptance_rate
            wait_score = min(candidate.wait_time_seconds / 1800, 1.0)
            
            # Weighted sum
            candidate.score = (
                self.WEIGHTS["eta"] * eta_score +
                self.WEIGHTS["rating"] * max(0, rating_score) +
                self.WEIGHTS["acceptance"] * acceptance_score +
                self.WEIGHTS["wait_time"] * wait_score
            )
        
        return candidates
    
    async def _dispatch_to_driver(
        self,
        request: MatchRequest,
        candidate: DriverCandidate
    ) -> bool:
        """Send ride request to driver and wait for response."""
        # Mark driver as unavailable (prevent double-dispatch)
        await self.driver_service.set_pending(candidate.driver_id)
        
        try:
            # Send push notification
            await self.notifications.send_ride_request(
                driver_id=candidate.driver_id,
                request_id=request.request_id,
                pickup_address="123 Main St",  # Would resolve address
                eta_seconds=candidate.eta_seconds
            )
            
            # Wait for response with timeout
            response = await asyncio.wait_for(
                self.driver_service.wait_for_response(
                    candidate.driver_id, 
                    request.request_id
                ),
                timeout=self.DRIVER_RESPONSE_TIMEOUT
            )
            
            return response == "accepted"
            
        except asyncio.TimeoutError:
            return False
            
        finally:
            # Release driver if not matched
            await self.driver_service.clear_pending(candidate.driver_id)

Deep Dive 4: ETA Prediction (Week 8 Concepts)#

Interviewer

"How do you calculate accurate ETAs?"

The Problem#

ETA challenges
Simple distance/speed calculation fails because
  • Traffic varies by time of day
  • Accidents, road closures
  • Weather affects speed
  • One-way streets
  • Pickup location accessibility (building entrance vs street)
  • Historical patterns matter
Uber's DeepETA model
  • Our target (this design's SLO, not a published Uber figure): 95% of ETAs within 3 minutes
  • Deep-learning model that predicts a correction on top of the routing engine's ETA

Implementation#

pythonETAResult · 129 lines
# routing/eta_service.py

"""
ETA Prediction Service

Combines routing engine with ML model for accurate
arrival time predictions.
"""

from dataclasses import dataclass
from typing import List, Optional, Tuple
from datetime import datetime


@dataclass
class ETAResult:
    eta_seconds: int
    distance_meters: int
    confidence: float
    route_polyline: str


class ETAService:
    """
    Calculate estimated time of arrival.
    
    Combines:
    - Graph-based shortest path (baseline)
    - Real-time traffic data
    - ML model adjustments (DeepETA)
    - Historical patterns
    
    Applies concepts:
    - Week 8: ML pipeline integration
    - Week 4: Caching for repeated queries
    """
    
    def __init__(
        self, 
        routing_engine,
        traffic_service,
        ml_model,
        cache
    ):
        self.routing = routing_engine
        self.traffic = traffic_service
        self.model = ml_model
        self.cache = cache
    
    async def calculate(
        self,
        origin_lat: float,
        origin_lng: float,
        dest_lat: float,
        dest_lng: float,
        departure_time: Optional[datetime] = None
    ) -> ETAResult:
        """Calculate ETA from origin to destination."""
        departure_time = departure_time or datetime.utcnow()
        
        # Check cache for recent identical query
        cache_key = self._make_cache_key(
            origin_lat, origin_lng, dest_lat, dest_lng
        )
        cached = self.cache.get(cache_key)
        if cached:
            return cached
        
        # Step 1: Get baseline route from routing engine
        route = await self.routing.get_route(
            origin_lat, origin_lng,
            dest_lat, dest_lng
        )
        
        # Step 2: Get current traffic conditions
        traffic_factor = await self.traffic.get_factor(
            route.segments,
            departure_time
        )
        
        # Step 3: ML model adjustment
        features = self._extract_features(
            route, traffic_factor, departure_time
        )
        ml_adjustment = self.model.predict(features)
        
        # Step 4: Combine estimates
        base_eta = route.duration_seconds
        traffic_eta = base_eta * traffic_factor
        final_eta = int(traffic_eta * ml_adjustment)
        
        result = ETAResult(
            eta_seconds=final_eta,
            distance_meters=route.distance_meters,
            confidence=0.95 if traffic_factor < 1.5 else 0.85,
            route_polyline=route.polyline
        )
        
        # Cache for 60 seconds
        self.cache.setex(cache_key, 60, result)
        
        return result
    
    def _extract_features(
        self,
        route,
        traffic_factor: float,
        departure_time: datetime
    ) -> dict:
        """Extract features for ML model."""
        return {
            "distance_km": route.distance_meters / 1000,
            "base_duration_min": route.duration_seconds / 60,
            "traffic_factor": traffic_factor,
            "hour_of_day": departure_time.hour,
            "day_of_week": departure_time.weekday(),
            "is_rush_hour": self._is_rush_hour(departure_time),
            "num_turns": route.num_turns,
            "num_traffic_lights": route.num_traffic_lights,
            "highway_fraction": route.highway_fraction,
        }
    
    def _is_rush_hour(self, dt: datetime) -> bool:
        """Check if time is during typical rush hour."""
        hour = dt.hour
        is_weekday = dt.weekday() < 5
        morning_rush = 7 <= hour <= 9
        evening_rush = 17 <= hour <= 19
        return is_weekday and (morning_rush or evening_rush)
Quick check3 questions · +5 XP each

Matching and ETA

  1. Q1/3 Why does _dispatch_to_driver mark the driver as pending before sending the push notification?

    Answer & explanation

    Answer: D

    Without the pending flag, two matching requests could both pick the same top-scored driver. The flag prevents double-dispatch and is cleared if the driver declines or times out.

  2. Q2/3 During a rush, requests are batched in 2-second windows and solved as an assignment problem. What does that fix compared with matching one request at a time?

    Answer & explanation

    Answer: A

    Batching trades up to 2 seconds of waiting for a globally better pairing. Each driver still gets one request and can still decline.

  3. Q3/3 The routing engine says 10 minutes at baseline, the traffic factor is 1.5 and the ML adjustment is 1.1. What ETA does the service return?

    Answer & explanation

    Answer: C

    The pipeline multiplies: 10 x 1.5 = 15 minutes with traffic, then x 1.1 from the model = 16.5 minutes. The factors compound; they are not added.

Phase 5 · Stage 5 of 7 · 1 min

Phase 5: Scaling and Edge Cases#

Scaling Strategies#

Scaling strategies
Geographic sharding
  • Each city/region is a shard
  • Location data stays local (low latency)
  • Cross-region trips handled specially
Event-driven architecture
  • All state changes → Kafka events
  • Services react to events asynchronously
  • Enables real-time analytics pipeline
Cell-based load balancing
  • Hot H3 cells (downtown) get dedicated capacity
  • Cold cells share resources
  • Dynamic rebalancing based on demand
Hybrid cloud
  • Core services historically in Uber's own data centers
  • Moving to cloud (Uber signed multi-year deals with Oracle Cloud and Google Cloud in 2023)
  • Multi-region active-active

Edge Cases#

Edge cases
Edge case 1: Driver goes offline mid-match

Scenario: Driver selected, then loses connectivity

Handling:

  • 15-second timeout for response
  • Automatic retry with next best driver
  • Driver marked unavailable after 30s silence
Edge case 2: Mass event ending (Concert, Sports)

Scenario: 50,000 people request rides simultaneously

Handling:

  • Surge pricing activates (attracts drivers)
  • Virtual queue system for fairness
  • Pre-positioning drivers before event ends
  • Predictive scaling based on event schedule
Edge case 3: GPS drift/inaccuracy

Scenario: Driver appears in wrong H3 cell

Handling:

  • Kalman filter smooths location jumps
  • Query neighboring cells (k-ring)
  • Fall back to larger radius search
Edge case 4: Payment failure

Scenario: Card declined after trip completion

Handling:

  • Retry with backup payment method
  • Grace period before blocking rider
  • Driver always gets paid (Uber absorbs risk)
Quick check3 questions · +5 XP each

Scaling and edge cases

  1. Q1/3 Why does geographic sharding (each city or region is a shard) fit Uber so well?

    Answer & explanation

    Answer: B

    Matching, surge and location lookups are inherently local, so a city shard keeps data close and queries single-shard. Load still varies inside a city, which is why hot H3 cells get dedicated capacity; cross-region trips need special handling.

  2. Q2/3 The selected driver's phone loses signal right after the ride request is pushed. What happens next?

    Answer & explanation

    Answer: D

    The dispatch loop treats a timeout like a decline and tries the next candidate. Separately, the driver's 30 s TTL removes them from future searches.

  3. Q3/3 GPS drift places a driver one cell away from where they really are. Which handling keeps them matchable?

    Answer & explanation

    Answer: A

    The search never relies on a single cell: the k-ring covers neighbors, and the filter removes implausible jumps. Finer hexagons would make drift cross more cell borders, not fewer.

Phase 6 · Stage 6 of 7 · 1 min

Phase 6: Monitoring and Operations#

Uber monitoring dashboard (illustrative numbers)
Marketplace health
Metric Value Reading
Trips completing/minute ~21,000 20,834
Average ETA accuracy 94.7% 94.7%
Match success rate 98.2% 98.2%
Average surge 1.15x 1.15x
Latency
Metric Value Status
Match time (p50) 1.8s on target
Match time (p99) 4.2s warning
Location update processing 45ms healthy
ETA calculation 180ms healthy
Supply-demand
Metric Value
Online drivers 1.2M
Active trips 890K
Pending requests 12K
Surge zones (>1.5x) 847
Infrastructure
Metric Value Status
Kafka lag < 100ms healthy
Redis hit rate 99.2% healthy
API error rate 0.02% healthy
WebSocket connections 45M

Alerting Strategy#

Alerting strategy

CRITICAL (PagerDuty)

  • Match success rate < 90%
  • Trip completion rate < 95%
  • Location service latency > 500ms
  • Payment processing failures > 1%

WARNING (Slack)

  • Surge zones > 2000 (unusual demand)
  • Driver online rate dropping > 10%
  • ETA accuracy < 90%
  • Kafka consumer lag > 1 minute

INFO (Dashboard)

  • Regional traffic patterns
  • Driver earnings distribution
  • New city launch metrics
Quick check3 questions · +5 XP each

Marketplace health

  1. Q1/3 Match time p50 is 1.8 s, but p99 is 4.2 s against a 3 s target. What most often creates that slow tail?

    Answer & explanation

    Answer: B

    The search itself is fast. The time goes into offers: each unanswered offer can cost up to 15 seconds, so declines and silent drivers dominate the tail.

  2. Q2/3 Why is "location service latency > 500 ms" critical while "Kafka consumer lag > 1 minute" is only a warning?

    Answer & explanation

    Answer: A

    Slow or stale positions mean bad matches and wrong ETAs right now. Lag on downstream consumers delays analytics and dashboards, which matters, but not to the rider waiting at the kerb.

  3. Q3/3 The number of surge zones suddenly jumps past 2,000. What should you check first?

    Answer & explanation

    Answer: B

    Surge is a ratio. A big event pushes demand up; an app release that logs drivers out pushes supply down. The fix for each is completely different.

Final challenge · Stage 7 of 7 · 2 min

Final Challenge: The Region Fails at Rush Hour#

The interviewer glances at the clock. "Last scenario, and it's an ugly one."

Interviewer

"It's Friday at 6 PM. The region serving a large city goes down, with 200,000 trips in progress and new requests arriving every second. What happens to those trips, and how quickly can people book rides again?"

The outage brief
  • One region is gone; another region is healthy
  • 200,000 trips are in progress, with fares quoted and drivers driving
  • Driver and rider apps are still running and still have GPS
  • New ride requests keep arriving
Your turn · checkpoint 5not answered

Which state is precious, and which rebuilds itself?

Sort the system's state into what must survive exactly and what can be rebuilt. Then describe the failover from the apps' point of view.

Think it through, or jot it down. Then compare.

One strong answer

  • Precious: trip state. Who is riding with whom, the locked upfront fare and the trip's status are replicated to the other region (active-active), so in-progress trips can be served from there.
  • Rebuildable: the location index. It's soft state, and every online driver sends a fresh position every 4 seconds, so the healthy region rebuilds it within a ping cycle or two of the apps reconnecting. No need to replicate it.
  • The apps carry on: drivers keep navigating, and both apps hold the current trip locally and keep recording the GPS trail, then resync once they reconnect to the healthy region.
  • Make every step idempotent: completing a trip or charging for it is keyed by the trip ID, so calls retried during the failover can't create duplicate trips or double charges.
  • New requests: routed to the healthy region and matched as soon as its location index has warmed up; scale that region out ahead of time, because it now carries the extra load.
Quick check4 questions · +5 XP each

Final challenge · Quest check

  1. Q1/4 Why doesn't the live driver-location index need to be replicated to the backup region?

    Answer & explanation

    Answer: B

    The index only answers "where are drivers right now?". Fresh pings rebuild it quickly, so replicating it would cost a lot and buy almost nothing.

  2. Q2/4 Which data must survive the failover exactly as it was?

    Answer & explanation

    Answer: B

    Surge, ETAs and locations are recomputed from live data. Trips are commitments to riders and drivers, and they must not be lost, duplicated or re-priced.

  3. Q3/4 During the failover a driver taps "Complete trip" twice because the first call timed out. What prevents a double charge?

    Answer & explanation

    Answer: C

    Exactly-once effects come from idempotent operations: the same trip ID always maps to one completion and one charge, however many times it's requested.

  4. Q4/4 Suppose the failed region served 300,000 online drivers, each pinging every 4 seconds. How many extra location updates a second must the healthy region absorb once their apps reconnect?

    Answer & explanation

    Answer: B

    300,000 ÷ 4 s = 75,000 updates a second on top of the healthy region's own traffic, which is why it needs headroom planned before the outage, not provisioned during it.

Wrap-up · 2 min

Interview Conclusion#

Transcript
Interviewer

"Excellent work. You've covered the geospatial system, pricing, and matching really well. Any questions?"

You

"Thank you! I'm curious about how Uber handles the transition when a driver crosses city boundaries—does the surge pricing change mid-trip?"

Interviewer

"Great question. The fare is locked at request time based on pickup location. If surge changes during the trip, the rider pays what was quoted upfront. We introduced upfront pricing specifically to avoid surprises."

Summary: Concepts Applied#

Concepts from 10-week course in Uber design
  • Week 1: Data at scale
    • H3 Partitioning — Geospatial sharding by hexagon
    • Geographic sharding — City-level data isolation
    • Hot key handling — High-demand cells get dedicated capacity
  • Week 2: Failure-first design
    • Timeouts — 15s driver response window
    • Retries — Automatic failover to next driver
    • Circuit breakers — Protect downstream services
  • Week 3: Messaging & async
    • Kafka — Trillions of events per day
    • Event-driven — All state changes as events
    • Real-time processing — Flink for surge calculation
  • Week 4: Caching
    • Redis — Driver locations for sub-ms lookups
    • ETA cache — Avoid redundant routing calculations
    • Surge cache — 30-second TTL for stability
  • Week 5: Consistency
    • Strong — Trip state (Spanner)
    • Eventual — Driver locations, surge pricing
    • Optimistic locking — Prevent double-dispatch
  • Week 8: Analytics pipeline
    • Real-time — Surge pricing, fraud detection
    • Batch — ML model training, demand forecasting
    • ML integration — DeepETA, demand prediction
  • Week 10: Production readiness
    • SLOs — < 3s match, 95% ETA accuracy
    • Multi-region — Active-active deployment
    • Observability — Real-time marketplace health
Why Uber is an engineering marvel
Real-time scale
  • ~43 million trips a day, rides + deliveries (Q2 2026)
  • Hundreds of thousands of location updates per second (our estimate)
  • Matching in seconds (this design targets < 3 s)
Innovation
  • H3: Open-sourced hexagonal geospatial system
  • DeepETA: deep-learning correction on top of routing-engine ETAs
  • Dynamic pricing: Real-time market equilibrium
Global complexity
  • 15,000+ cities with local regulations
  • Multiple products: Rides, Eats, Freight
  • ~10 million monthly active drivers and couriers
Data platform
  • 200,000+ data pipelines
  • Petabytes processed daily
  • Thousands of ML models in production

Sources#

Official Uber Engineering:

H3 Documentation:

Architecture:

Company figures:

Pricing:

Self-Assessment Checklist#

After studying this case study, you should be able to:

After studying this case study, you should be able to0/11 ready
  • Explain why hexagons are superior to squares for geospatial grids
  • Design an H3-based location index with O(1) lookups
  • Implement a surge pricing algorithm with demand-supply balancing
  • Design a driver-rider matching system with weighted scoring
  • Calculate ETAs combining routing, traffic, and ML models
  • Handle edge cases: GPS drift, mass events, driver cancellations
  • Design for global scale with geographic sharding
  • Implement real-time location tracking with WebSockets
  • Balance consistency needs (strong for trips, eventual for locations)
  • Build observability for a real-time marketplace
  • Articulate trade-offs in two-sided marketplace design

This case study covers Uber's architecture for matching millions of riders with drivers in real-time across 15,000+ cities worldwide.

this lesson in shorts · 3

play them all →

One idea from this lesson in about a minute each, made for your phone.

End of Side quest 09 · Uber
Clear the questFinish Uber.Stages clear as you read them. Marking complete unlocks after ~6 more min of reading.
  • 0/7stages cleared

Follow-up drills

0 of 10 practised

You presented Uber. Now the interviewer pushes back. Answer each follow-up out loud or in the box, then reveal a strong answer.

Follow-up 1 / 10 · scale & numbers

“Which write rate shapes your storage design: trips or locations?”

Pushes on · sizing by the dominant write stream

Revealing marks it practised.
design sheet · 7 diagrams · pdfRevise the whole design in ten minutesThe numbers, architecture, sequence, class and state diagrams, decisions and failure modes of this quest on a few printable pages.open the sheet →

Go deeper · reading trail

all reading →

Real write-ups from the teams that built these systems. Each opens on the publisher's site in a new tab.

UberBlog post · ~10 min

H3: Uber's Hexagonal Hierarchical Spatial Index

Isaac Brodsky · 2018
What
Uber introduces H3, its open-source hexagonal grid system.
Key idea
Hexagons have uniform neighbour distances and nest across resolutions, which suits analysing supply, demand and pricing by area.
Why here
The H3 deep dive of this quest at its source, including why hexagons beat squares for surge pricing cells.
taught here: H3 geospatial indexing
UberBlog post · ~15 min

DeepETA: How Uber Predicts Arrival Times Using Deep Learning

2022
What
How Uber improved its ETA predictions with a deep learning model.
Key idea
A routing engine computes an ETA from map data and live traffic, and a deep model predicts the residual correction on top of it.
Why here
The ETA deep dive of this quest: physical model plus learned correction, served at very high request rates.
taught here: ETA prediction
UberBlog post · ~19 min

Uber's Fulfillment Platform: Ground-up Re-architecture to Accelerate Uber's Go/Get Strategy

2021
What
Uber's rebuild of the platform that holds the state of every trip and driver session.
Key idea
The old Ringpop-based design favoured availability; the new one moved to a NewSQL store with strongly consistent transactions to support many products.
Why here
The matching flow of this quest needs one source of truth for each trip and driver, which is what this platform provides.
taught here: Matching and trip state

New lessons by email

New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.

Discussion · …

Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

Sign in to ask a question or answer one. Reading stays open to everyone.
loading the discussion…