Skip to content

🚗 System Design: Uber / Ride-Sharing

Location-based, real-time matching at global scale.


Step 1: Requirements

Functional

  • Rider requests a ride
  • Match rider with nearby driver
  • Real-time location tracking of driver
  • Dynamic pricing (surge pricing)
  • ETA calculation
  • Route navigation

Non-Functional

  • 10M trips/day, 5M active drivers
  • Location updates every 4 seconds
  • Match should happen in < 3 seconds
  • 99.99% uptime

Step 2: Capacity

Location updates:
  5M active drivers × every 4 sec = 1.25M location writes/sec!

Matching requests:
  10M trips/day = ~115 trip requests/sec

Step 3: Core Problem — Geospatial Matching

How do you find drivers within 2km of a rider efficiently?

NAIVE approach:
  SELECT * FROM drivers WHERE lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?
  ❌ Slow, no spatial indexing

SOLUTION: Geohash (or S2 cells like Uber uses)
  Divide the world into a grid of cells
  Each cell has a hash string: "9q8yy" = San Francisco area
  Encode lat/lng → geohash prefix

  Driver at (37.77, -122.41) → geohash "9q8yy"

  Find nearby drivers:
  1. Get geohash of rider location
  2. Find 8 neighboring cells
  3. Retrieve all drivers in those cells

  ✅ O(1) lookup from Redis!

Step 4: Architecture

Example: Location Service updating Redis

javascript
import Redis from "ioredis";

const redis = new Redis();

async function updateDriverLocation(driverId, lat, lng) {
  // Add or update the driver's location in the geospatial index
  // The 'driver_locations' key holds the geospatial data
  await redis.geoadd("driver_locations", lng, lat, driverId);

  // Optionally set an expiry if a driver hasn't pinged recently
  // This requires a separate ZSET or TTL key per driver in standard Redis
  await redis.setex(`driver_status:${driverId}`, 10, "ONLINE");

  console.log(`Updated location for driver ${driverId}`);
}

Step 5: Location Update Flow

Every 4 seconds:
  Driver App ──► Location Service ──► Kafka

                         ┌───────────────┘

                  Location Consumer

                 ┌───────┴───────┐
                 ▼               ▼
          Redis (current      Cassandra (historical
          location +          location trail for
          geohash index)      route/analytics)

Step 6: Matching Flow

Example: Trip Matching Service querying Redis

javascript
import Redis from "ioredis";

const redis = new Redis();

async function findNearbyDrivers(riderLat, riderLng, radiusKm = 2) {
  // Query Redis for drivers within the specified radius
  // Returns array of arrays: [[driverId, distance], ...]
  const nearbyDrivers = await redis.geosearch(
    "driver_locations",
    "FROMLONLAT",
    riderLng,
    riderLat,
    "BYRADIUS",
    radiusKm,
    "km",
    "WITHDIST", // Include distance in response
    "ASC", // Sort nearest first
    "COUNT",
    10 // Limit to top 10 closest
  );

  const availableDrivers = [];

  for (const [driverId, distance] of nearbyDrivers) {
    // Check if the closer drivers are actually available/online
    const status = await redis.get(`driver_status:${driverId}`);
    if (status === "ONLINE") {
      availableDrivers.push({ driverId, distance });
    }
  }

  return availableDrivers;
}

Step 7: Surge Pricing

Example: Calculating Surge Pricing

javascript
import Redis from "ioredis";

const redis = new Redis();

// This would typically run in a stream processor or a cron job every minute
async function calculateSurgeMultiplier(geohashCell) {
  // In reality, these metrics might come from a time-series DB or stream aggregation
  const demandCount = parseInt((await redis.get(`demand:${geohashCell}`)) || 0);
  const supplyCount = parseInt((await redis.get(`supply:${geohashCell}`)) || 1); // Avoid div by 0

  const ratio = demandCount / supplyCount;
  let multiplier = 1.0;

  if (ratio > 3.0)
    multiplier = 2.0; // 2.0x surge
  else if (ratio > 2.0)
    multiplier = 1.5; // 1.5x surge
  else if (ratio > 1.2) multiplier = 1.2; // 1.2x surge

  // Store the new multiplier in Redis for quick access by pricing services
  await redis.set(`surge_multiplier:${geohashCell}`, multiplier);

  // Reset demand counters for the next window
  await redis.set(`demand:${geohashCell}`, 0);

  return multiplier;
}

Step 8: Trip Lifecycle

Example: Trip State Machine

javascript
class TripStateMachine {
  constructor(tripId) {
    this.tripId = tripId;
    this.state = "REQUESTED";
  }

  transition(event) {
    switch (this.state) {
      case "REQUESTED":
        if (event === "DRIVER_ACCEPTED") this.state = "ACCEPTED";
        if (event === "RIDER_CANCELLED") this.state = "CANCELLED";
        break;
      case "ACCEPTED":
        if (event === "DRIVER_MOVING") this.state = "DRIVER_EN_ROUTE";
        if (event === "DRIVER_CANCELLED") this.state = "REQUESTED"; // Rematch
        break;
      case "DRIVER_EN_ROUTE":
        if (event === "DRIVER_ARRIVED") this.state = "ARRIVED";
        break;
      case "ARRIVED":
        if (event === "TRIP_STARTED") this.state = "IN_TRIP";
        break;
      case "IN_TRIP":
        if (event === "TRIP_ENDED") this.state = "COMPLETED";
        break;
    }
    console.log(`Trip ${this.tripId} transitioned to ${this.state}`);
  }
}

const trip = new TripStateMachine("trip_123");
trip.transition("DRIVER_ACCEPTED"); // ACCEPTED
trip.transition("DRIVER_MOVING"); // DRIVER_EN_ROUTE

📊 Summary

ComponentTechnology
Location UpdatesKafka + Redis GeoHash
Geospatial IndexRedis Geo (S2 Cells internally)
Driver ↔ App commsWebSockets
Historical tripsCassandra
Trip state machineRedis + PostgreSQL
Route calculationGoogle Maps API / OSRM
Surge pricingStream processing (Flink/Spark)

Key insight: The geospatial indexing with geohash/S2 cells is the core performance trick. Without it, matching would require scanning millions of driver locations.

Released under the ISC License.