Engineering Atlas › Low-Level Design

Code a ride-hailing service with driver matching

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

Requirements

Functional:

Non-functional:

Out of scope: the geospatial index at city scale, ETA prediction, payments, ratings and shared rides. The Design Uber system design lesson covers the location firehose, geo-sharding and the real-time connections. This lesson is the object model and the in-process logic of one matching cell.

Entities and relationships

Relationships: the service owns drivers and trips. A trip references drivers by id: one current offer, one assignment, many already tried. A driver does not point back to the trip in the model; the service keeps that index, so there is one source of truth for "who holds whom".

Class diagram

classDiagram
    class RideService {
      -drivers: Map~DriverId, Driver~
      -trips: Map~TripId, Trip~
      -clock: Clock
      +request(rider, pickup, surge) TripId
      +respond(driver, trip, accept) Result
      +tick()
      +start(trip, driver)
      +finish(trip, driver, meters) Money
      +cancel(trip, rider) Money
    }
    class Driver {
      +id: DriverId
      +location: Location
      +state: DriverState
    }
    class Trip {
      +id: TripId
      +rider: RiderId
      +pickup: Location
      +surgePct: int
      -tried: Set~DriverId~
      -offeredTo: DriverId
      -offeredAt: int
      +driver: DriverId
      +status: TripStatus
    }
    class DriverLocator {
      <<interface>>
      +nearestAvailable(point, exclude) DriverId[]
    }
    class PricingPolicy {
      <<interface>>
      +fare(meters, seconds, surgePct) Money
      +cancelFee(sinceAssigned) Money
    }
    class Clock {
      <<interface>>
      +now() int
    }
    RideService "1" *-- "many" Driver
    RideService "1" *-- "many" Trip
    Trip --> Driver : offered or assigned
    RideService --> DriverLocator
    RideService --> PricingPolicy
    RideService --> Clock
    GridLocator ..|> DriverLocator
    MeteredPricing ..|> PricingPolicy

Key flows

A request where the nearest driver declines, the second lets the offer time out, and the third accepts.

sequenceDiagram
    participant R as Rider
    participant S as RideService
    participant L as DriverLocator
    participant A as Driver A
    participant B as Driver B
    participant C as Driver C
    R->>S: request(pickup, surge 150)
    S->>L: nearestAvailable(pickup, tried)
    L-->>S: A, B, C
    S->>A: offer (A held)
    A->>S: decline
    S->>B: offer (A released, B held)
    Note over S,B: 15 s pass, tick expires offer
    S->>C: offer (B released, C held)
    C->>S: accept
    S-->>R: driver C assigned
    C->>S: start, then finish(4300 m)
    S-->>R: fare 1599

Design patterns used and why

Concurrency and edge cases

Down the stack

Layer 1The contract

request returns a trip that is either offered to exactly one available driver (now held) or ended as no drivers. respond from the offered driver within the timeout either assigns the trip or moves the offer to the next nearest untried driver; any other response changes nothing visible to that driver. After tick, no offer older than the timeout is outstanding. finish returns max(minimum, (base + per_km × meters / 1000 + per_min × seconds / 60) × surge / 100) with floor division at each step. cancel releases any held or assigned driver and returns the fee that the grace rule implies.

Layer 2The mechanism

In one process: a hash map of drivers, a hash map of trips and one mutex around both, because every interesting operation touches a trip and a driver together. Picking the next driver sorts available, untried drivers by distance then id. At city scale this becomes a grid or H3 index: the pickup's cell and its rings are scanned outwards until enough candidates are found, which is the Design Uber lesson's core. Offer expiry is a min-heap keyed by offeredAt + timeout, with stale entries skipped on pop because the trip may already have moved on; a hierarchical timing wheel does the same with O(1) inserts when there are millions of timers. When the state lives in a database, the hold is a conditional update, UPDATE drivers SET state = 'offered', trip_id = $1 WHERE id = $2 AND state = 'available', and a zero row count means another matcher got that driver first; try the next candidate.

Layer 3The machine

The mutex is never the bottleneck. A busy city might see 50 requests per second and a few hundred driver responses; each critical section is microseconds of work (sorting a few dozen nearby candidates and flipping two enums), against a lock cost of roughly 20 ns uncontended. The real latency is the network to the driver's phone: a push over a mobile connection takes 100 ms to several seconds, which is why the offer timeout is measured in seconds, not milliseconds. One more machine-level fact matters for fairness: timeouts must use a monotonic clock, because an NTP step on the server could otherwise expire every outstanding offer at once.

Why it's built this way

  1. Drivers are scarce and riders wait in real time, so a driver must not be offered two trips at once.
  2. Drivers answer through slow, unreliable phones, so each offer needs a hold with a deadline rather than a blocking call.
  3. Deadlines and fares must be testable and reproducible, so time comes from an injected clock and fare inputs are snapshotted on the trip.
  4. The nearest-driver query changes with scale while the offer protocol does not, so the locator sits behind an interface.
  5. Matching decisions touch a trip and a driver together, so they share one serialisation point per matching cell, and cells scale out by geography.

Extensibility

Leadership lens

Check yourself

  1. Why must choosing a driver and marking them "offered" happen in one critical section?

    Show answer

    Otherwise two concurrent requests can both read the same driver as available and both offer to them. Combining the read and the hold (a lock or a conditional update) makes one request win and the other move on.

  2. A driver answers "accept" one second after the offer timeout, before the expiry job has run. What should happen?

    Show answer

    The response handler checks the deadline itself and treats the offer as expired, moving to the next driver. The result must not depend on whether the sweep ran first.

  3. Why snapshot surge on the trip instead of reading it at drop-off?

    Show answer

    The rider agreed to a price at request time, and surge can change during the ride. Recording it makes the fare reproducible and fair.

  4. Why keep a set of drivers already tried on the trip?

    Show answer

    Without it, a decline or timeout can return the trip to the same driver, who is still the nearest, producing a loop that never reaches other drivers.

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →