Design a Ride-Sharing Service

Difficulty: Advanced

Question

Design a ride-sharing service like Uber or Lyft. How would you match riders with nearby drivers and provide real-time tracking?

Answer

A ride-sharing service connects riders with nearby drivers in real-time, manages trips, and handles payments.

Core services: 1. Location Service: Track real-time driver positions using geospatial indexing 2. Matching Service: Find and assign the best nearby driver for a ride request 3. Trip Service: Manage ride lifecycle (request -> match -> pickup -> trip -> dropoff) 4. Pricing Service: Calculate fare with dynamic/surge pricing 5. ETA Service: Estimate arrival times using road network data 6. Payment Service: Charge rider, pay driver

Key technical challenges: - Millions of concurrent driver location updates (every 3-5 seconds) - Fast geospatial queries (find drivers within 5km) - Real-time matching under high demand - Accurate ETA calculations - Surge pricing algorithm

Code examples

Geospatial Driver Tracking

// Track driver locations using Redis GEO
// Redis GEO uses sorted sets with geohash encoding

class LocationService {
  // Update driver location (called every 3-5 seconds)
  async updateDriverLocation(driverId, lat, lng) {
    // Store in Redis GEO set
    await redis.geoadd('drivers:active', lng, lat, driverId);

    // Store driver metadata
    await redis.hset(`driver:${driverId}`, {
      lat, lng,
      lastUpdate: Date.now(),
      status: 'AVAILABLE' // AVAILABLE, EN_ROUTE, IN_TRIP
    });
  }

  // Find nearby drivers within radius
  async findNearbyDrivers(lat, lng, radiusKm = 5) {
    // GEORADIUS returns drivers within radius, sorted by distance
    const nearby = await redis.georadius(
      'drivers:active',
      lng, lat,
      radiusKm, 'km',
      'WITHCOORD', 'WITHDIST',
      'ASC',  // Closest first
      'COUNT', 20
    );

    // Filter to only available drivers
    const available = [];
    for (const driver of nearby) {
      const meta = await redis.hgetall(`driver:${driver.key}`);
      if (meta.status === 'AVAILABLE') {
        available.push({
          driverId: driver.key,
          distance: parseFloat(driver.dist),
          location: { lat: driver.lat, lng: driver.lng },
          rating: meta.rating
        });
      }
    }

    return available;
  }

  // Remove offline driver
  async removeDriver(driverId) {
    await redis.zrem('drivers:active', driverId);
    await redis.del(`driver:${driverId}`);
  }
}

// Scale: for millions of drivers, partition by city/geohash
// drivers:active:mumbai, drivers:active:delhi, etc.

Redis GEO commands provide O(N+log(M)) geospatial queries where N is results and M is total entries. Partitioning by city keeps each set small and queries fast.

Ride Matching Algorithm

class MatchingService {
  async matchRider(rideRequest) {
    const { riderId, pickupLat, pickupLng } = rideRequest;

    // 1. Find nearby available drivers (expanding radius)
    let drivers = [];
    for (const radius of [2, 5, 10]) {
      drivers = await locationService.findNearbyDrivers(
        pickupLat, pickupLng, radius
      );
      if (drivers.length > 0) break;
    }

    if (drivers.length === 0) {
      return { matched: false, message: 'No drivers available' };
    }

    // 2. Score drivers (distance + rating + acceptance rate)
    const scored = drivers.map(d => ({
      ...d,
      score: this.calculateMatchScore(d)
    })).sort((a, b) => b.score - a.score);

    // 3. Send ride offer to best driver
    const bestDriver = scored[0];

    await redis.hset(`driver:${bestDriver.driverId}`, 'status', 'OFFERED');

    const offer = {
      rideId: generateId(),
      riderId,
      driverId: bestDriver.driverId,
      pickup: { lat: pickupLat, lng: pickupLng },
      estimatedPickupTime: bestDriver.distance * 2, // rough ETA in minutes
      expiresAt: Date.now() + 15000 // 15 seconds to accept
    };

    // Send offer via WebSocket
    await wsGateway.sendToDriver(bestDriver.driverId, {
      type: 'RIDE_OFFER',
      data: offer
    });

    // 4. If driver doesn't accept in 15s, try next driver
    setTimeout(async () => {
      const accepted = await redis.get(`offer:${offer.rideId}:accepted`);
      if (!accepted) {
        await redis.hset(`driver:${bestDriver.driverId}`, 'status', 'AVAILABLE');
        await this.matchRider(rideRequest); // Try next driver
      }
    }, 15000);

    return offer;
  }

  calculateMatchScore(driver) {
    const distanceScore = Math.max(0, 100 - driver.distance * 10);
    const ratingScore = (driver.rating || 4.0) * 20;
    return distanceScore * 0.7 + ratingScore * 0.3;
  }
}

The matching algorithm finds nearby drivers, scores them by distance and rating, and sends offers sequentially until one accepts. The expanding radius search prevents failures when drivers are sparse.

Surge Pricing

// Surge pricing balances supply (drivers) and demand (riders)
class PricingService {
  async calculateFare(pickup, dropoff) {
    // 1. Base fare calculation
    const distance = calculateDistance(pickup, dropoff); // km
    const estimatedDuration = await etaService.estimate(pickup, dropoff); // min

    const baseFare = 50; // base charge
    const perKm = 12;    // per kilometer
    const perMin = 2;    // per minute

    let fare = baseFare + (distance * perKm) + (estimatedDuration * perMin);

    // 2. Apply surge multiplier
    const surgeMultiplier = await this.getSurgeMultiplier(pickup);
    fare *= surgeMultiplier;

    return {
      baseFare: Math.round(fare),
      surgeMultiplier,
      distance,
      estimatedDuration,
      breakdown: {
        base: baseFare,
        distanceCharge: Math.round(distance * perKm),
        timeCharge: Math.round(estimatedDuration * perMin),
        surge: surgeMultiplier > 1 ? `${surgeMultiplier}x` : 'none'
      }
    };
  }

  async getSurgeMultiplier(location) {
    // Get supply/demand ratio for this area
    const geohash = computeGeohash(location.lat, location.lng, 5); // ~5km area

    const [drivers, requests] = await Promise.all([
      redis.get(`supply:${geohash}`) || 0,
      redis.get(`demand:${geohash}`) || 0
    ]);

    const supply = parseInt(drivers) || 1;
    const demand = parseInt(requests) || 0;
    const ratio = demand / supply;

    // Surge tiers
    if (ratio > 3) return 2.5;   // Very high demand
    if (ratio > 2) return 2.0;
    if (ratio > 1.5) return 1.5;
    if (ratio > 1) return 1.2;
    return 1.0;                   // No surge
  }
}

// Track supply/demand in real-time
// Increment demand counter on ride request
// Increment supply counter on driver going active
// Counters expire after 5 minutes (rolling window)

Surge pricing uses the ratio of ride requests to available drivers in each geographic area. Higher demand relative to supply increases the multiplier. Geohash divides the city into grid cells for localized pricing.

Key points

Concepts covered

Geospatial Indexing, Matching Algorithm, Real-Time Tracking, ETA Calculation, Surge Pricing