Episode 9 — System Design / 9.6 — LLD Problem Solving

9.6.e — Uber/Ola (Ride Sharing System)

Problem: Design a ride-sharing system where riders request trips, the system matches them with nearby available drivers, tracks the trip lifecycle, calculates fares, and maintains ratings.


Table of Contents

  1. Requirements
  2. Entities and Enums
  3. Class Diagram
  4. Trip Lifecycle
  5. Sequence Flow
  6. Complete Implementation
  7. Usage Walkthrough
  8. Edge Cases
  9. Key Takeaways

Requirements

Functional Requirements

  1. Riders can request a trip by providing pickup and drop locations.
  2. The system finds the nearest available driver within a radius.
  3. The driver can accept or decline the request.
  4. Once accepted, the trip moves through states: REQUESTED -> ACCEPTED -> IN_PROGRESS -> COMPLETED.
  5. Fare is calculated based on distance, time, and ride type (base fare + per-km + per-min).
  6. Both rider and driver can rate each other (1-5 stars).
  7. Riders and drivers can view their trip history.

Non-Functional Requirements

  • Driver matching should find the closest available driver.
  • Fare calculation should be extensible (surge pricing, promo codes).

Out of Scope

  • Real GPS tracking (we simulate with coordinates).
  • Payment processing.
  • Ride pooling / shared rides.
  • Mapping and route optimization.

Entities and Enums

┌────────────────────┐    ┌────────────────────┐    ┌────────────────────┐
│    TripStatus       │    │   DriverStatus      │    │    RideType         │
│────────────────────│    │────────────────────│    │────────────────────│
│  REQUESTED          │    │  AVAILABLE          │    │  ECONOMY            │
│  ACCEPTED           │    │  ON_TRIP            │    │  PREMIUM            │
│  IN_PROGRESS        │    │  OFFLINE            │    │  SUV                │
│  COMPLETED          │    └────────────────────┘    └────────────────────┘
│  CANCELLED          │
└────────────────────┘

Class Diagram

┌──────────────────────────────────┐       ┌──────────────────────────────┐
│           Rider                   │       │          Driver               │
│──────────────────────────────────│       │──────────────────────────────│
│ - id: string                      │       │ - id: string                  │
│ - name: string                    │       │ - name: string                │
│ - phone: string                   │       │ - phone: string               │
│ - location: Location              │       │ - location: Location          │
│ - rating: RatingTracker           │       │ - status: DriverStatus        │
│ - tripHistory: Trip[]             │       │ - vehicleNumber: string       │
│──────────────────────────────────│       │ - rideType: RideType          │
│ + requestTrip(pickup, drop): Trip │       │ - rating: RatingTracker       │
│ + rateDriver(trip, score): void   │       │ - tripHistory: Trip[]         │
└──────────────────────────────────┘       │──────────────────────────────│
                                            │ + accept(trip): void          │
                                            │ + decline(trip): void         │
                                            │ + startTrip(trip): void       │
                                            │ + completeTrip(trip): void    │
                                            │ + rateRider(trip, score): void│
                                            │ + goOnline(): void            │
                                            │ + goOffline(): void           │
                                            └──────────────────────────────┘

┌──────────────────────────────────┐       ┌──────────────────────────────┐
│          Location                 │       │       RatingTracker           │
│──────────────────────────────────│       │──────────────────────────────│
│ - latitude: number                │       │ - totalScore: number          │
│ - longitude: number               │       │ - count: number               │
│──────────────────────────────────│       │──────────────────────────────│
│ + distanceTo(other): number       │       │ + addRating(score): void      │
│                                   │       │ + getAverage(): number        │
└──────────────────────────────────┘       └──────────────────────────────┘

┌──────────────────────────────────┐       ┌──────────────────────────────┐
│            Trip                   │       │   FareCalculator (iface)      │
│──────────────────────────────────│       │──────────────────────────────│
│ - id: string                      │       │ + calculate(trip): number     │
│ - rider: Rider                    │       └──────────┬───────────────────┘
│ - driver: Driver | null           │                  │ implemented by
│ - pickup: Location                │       ┌──────────┴──────────┐
│ - drop: Location                  │       ▼                     ▼
│ - status: TripStatus              │  StandardFare         SurgeFare
│ - rideType: RideType              │
│ - fare: number                    │
│ - distance: number                │
│ - requestedAt: Date               │
│ - startedAt: Date | null          │
│ - completedAt: Date | null        │
│ - riderRating: number | null      │
│ - driverRating: number | null     │
└──────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────┐
│                          RideService                                  │
│──────────────────────────────────────────────────────────────────────│
│ - riders: Map<id, Rider>                                              │
│ - drivers: Map<id, Driver>                                            │
│ - trips: Map<id, Trip>                                                │
│ - fareCalculator: FareCalculator                                      │
│ - maxSearchRadiusKm: number                                           │
│──────────────────────────────────────────────────────────────────────│
│ + registerRider(name, phone): Rider                                   │
│ + registerDriver(name, phone, vehicleNum, rideType): Driver           │
│ + requestTrip(riderId, pickup, drop, rideType): Trip                  │
│ + findNearestDriver(location, rideType): Driver | null                │
│ + acceptTrip(driverId, tripId): void                                  │
│ + startTrip(tripId): void                                             │
│ + completeTrip(tripId): { fare, distance, duration }                  │
│ + cancelTrip(tripId): void                                            │
│ + rateTripByRider(tripId, score): void                                │
│ + rateTripByDriver(tripId, score): void                               │
└──────────────────────────────────────────────────────────────────────┘

Trip Lifecycle

  ┌────────────┐     driver found     ┌────────────┐
  │ REQUESTED  │ ──────────────────► │  ACCEPTED   │
  └─────┬──────┘                      └─────┬──────┘
        │                                    │
        │ no driver / rider cancels          │ driver starts ride
        │                                    │
        ▼                                    ▼
  ┌────────────┐                      ┌────────────┐
  │ CANCELLED  │                      │ IN_PROGRESS │
  └────────────┘                      └─────┬──────┘
                                             │
                                             │ driver completes ride
                                             │ (fare calculated)
                                             ▼
                                      ┌────────────┐
                                      │ COMPLETED   │
                                      └────────────┘

Sequence Flow

Complete Trip Flow

  Rider         RideService       DriverMatcher        Driver           FareCalc
    │                │                  │                  │                │
    │── requestTrip()►│                  │                  │                │
    │                │── findNearest() ─►│                  │                │
    │                │                  │── sort by dist ──►│               │
    │                │◄── driver ────────│                  │                │
    │                │── notify driver ────────────────────►│                │
    │◄── trip        │                  │                  │                │
    │   (REQUESTED)  │                  │                  │                │
    │                │                  │                  │                │
    │                │◄── acceptTrip() ────────────────────│                │
    │◄── ACCEPTED ───│                  │                  │                │
    │                │                  │                  │                │
    │                │◄── startTrip() ─────────────────────│                │
    │◄── IN_PROGRESS │                  │                  │                │
    │                │                  │                  │                │
    │                │◄── completeTrip() ──────────────────│                │
    │                │── calculate() ──────────────────────────────────────►│
    │                │◄── fare ────────────────────────────────────────────│
    │◄── COMPLETED   │                  │                  │                │
    │   { fare }     │                  │                  │                │
    │                │                  │                  │                │
    │── rateDriver() ►│                  │                  │                │
    │                │◄── rateRider() ─────────────────────│                │

Complete Implementation

Enums and Supporting Classes

// ─── Enums ──────────────────────────────────────────────

const TripStatus = Object.freeze({
  REQUESTED: 'REQUESTED',
  ACCEPTED: 'ACCEPTED',
  IN_PROGRESS: 'IN_PROGRESS',
  COMPLETED: 'COMPLETED',
  CANCELLED: 'CANCELLED',
});

const DriverStatus = Object.freeze({
  AVAILABLE: 'AVAILABLE',
  ON_TRIP: 'ON_TRIP',
  OFFLINE: 'OFFLINE',
});

const RideType = Object.freeze({
  ECONOMY: 'ECONOMY',
  PREMIUM: 'PREMIUM',
  SUV: 'SUV',
});

Location

// ─── Location ───────────────────────────────────────────

class Location {
  constructor(latitude, longitude) {
    this.latitude = latitude;
    this.longitude = longitude;
  }

  // Haversine formula for distance in km
  distanceTo(other) {
    const R = 6371; // Earth radius in km
    const dLat = this._toRad(other.latitude - this.latitude);
    const dLon = this._toRad(other.longitude - this.longitude);
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(this._toRad(this.latitude)) *
        Math.cos(this._toRad(other.latitude)) *
        Math.sin(dLon / 2) *
        Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c;
  }

  _toRad(deg) {
    return (deg * Math.PI) / 180;
  }

  toString() {
    return `(${this.latitude.toFixed(4)}, ${this.longitude.toFixed(4)})`;
  }
}

RatingTracker

// ─── RatingTracker ──────────────────────────────────────

class RatingTracker {
  constructor() {
    this.totalScore = 0;
    this.count = 0;
  }

  addRating(score) {
    if (score < 1 || score > 5) {
      throw new Error('Rating must be between 1 and 5.');
    }
    this.totalScore += score;
    this.count++;
  }

  getAverage() {
    if (this.count === 0) return 0;
    return Math.round((this.totalScore / this.count) * 100) / 100;
  }
}

Rider and Driver

// ─── Rider ──────────────────────────────────────────────

class Rider {
  static _counter = 0;

  constructor(name, phone) {
    this.id = `RIDER-${++Rider._counter}`;
    this.name = name;
    this.phone = phone;
    this.location = null;
    this.rating = new RatingTracker();
    this.tripHistory = [];
  }

  setLocation(location) {
    this.location = location;
  }
}

// ─── Driver ─────────────────────────────────────────────

class Driver {
  static _counter = 0;

  constructor(name, phone, vehicleNumber, rideType) {
    this.id = `DRIVER-${++Driver._counter}`;
    this.name = name;
    this.phone = phone;
    this.vehicleNumber = vehicleNumber;
    this.rideType = rideType; // RideType enum
    this.location = null;
    this.status = DriverStatus.OFFLINE;
    this.rating = new RatingTracker();
    this.tripHistory = [];
  }

  setLocation(location) {
    this.location = location;
  }

  goOnline() {
    this.status = DriverStatus.AVAILABLE;
  }

  goOffline() {
    if (this.status === DriverStatus.ON_TRIP) {
      throw new Error('Cannot go offline while on a trip.');
    }
    this.status = DriverStatus.OFFLINE;
  }
}

Trip

// ─── Trip ───────────────────────────────────────────────

class Trip {
  static _counter = 0;

  constructor(rider, pickup, drop, rideType) {
    this.id = `TRIP-${++Trip._counter}`;
    this.rider = rider;
    this.driver = null;
    this.pickup = pickup;
    this.drop = drop;
    this.rideType = rideType;
    this.status = TripStatus.REQUESTED;
    this.fare = 0;
    this.distance = pickup.distanceTo(drop);
    this.requestedAt = new Date();
    this.startedAt = null;
    this.completedAt = null;
    this.riderRating = null;   // Rating given BY rider (to driver)
    this.driverRating = null;  // Rating given BY driver (to rider)
  }
}

Fare Calculators (Strategy Pattern)

// ─── Fare Calculators ───────────────────────────────────

// Rate config per ride type
const FARE_RATES = Object.freeze({
  [RideType.ECONOMY]: { baseFare: 30, perKm: 8, perMin: 1.5 },
  [RideType.PREMIUM]: { baseFare: 50, perKm: 14, perMin: 2.5 },
  [RideType.SUV]: { baseFare: 80, perKm: 18, perMin: 3.0 },
});

class StandardFareCalculator {
  calculate(trip) {
    const rates = FARE_RATES[trip.rideType];
    if (!rates) throw new Error(`Unknown ride type: ${trip.rideType}`);

    const distanceKm = trip.distance;
    const durationMin = trip.completedAt && trip.startedAt
      ? (trip.completedAt - trip.startedAt) / (1000 * 60)
      : 0;

    const fare = rates.baseFare + rates.perKm * distanceKm + rates.perMin * durationMin;
    return Math.round(fare * 100) / 100;
  }
}

class SurgeFareCalculator {
  constructor(surgeMultiplier = 1.5) {
    this.surgeMultiplier = surgeMultiplier;
    this.standard = new StandardFareCalculator();
  }

  calculate(trip) {
    const baseFare = this.standard.calculate(trip);
    return Math.round(baseFare * this.surgeMultiplier * 100) / 100;
  }
}

RideService (Controller)

// ─── RideService ────────────────────────────────────────

class RideService {
  constructor() {
    this.riders = new Map();    // riderId -> Rider
    this.drivers = new Map();   // driverId -> Driver
    this.trips = new Map();     // tripId -> Trip
    this.fareCalculator = new StandardFareCalculator();
    this.maxSearchRadiusKm = 10;
  }

  // ── Registration ──────────────────────────────────────

  registerRider(name, phone) {
    const rider = new Rider(name, phone);
    this.riders.set(rider.id, rider);
    console.log(`[REGISTER] Rider: ${rider.name} (${rider.id})`);
    return rider;
  }

  registerDriver(name, phone, vehicleNumber, rideType) {
    const driver = new Driver(name, phone, vehicleNumber, rideType);
    this.drivers.set(driver.id, driver);
    console.log(`[REGISTER] Driver: ${driver.name} (${driver.id}) - ${rideType}`);
    return driver;
  }

  // ── Driver matching ───────────────────────────────────

  findNearestDriver(pickupLocation, rideType) {
    let nearest = null;
    let minDistance = Infinity;

    for (const [, driver] of this.drivers) {
      if (driver.status !== DriverStatus.AVAILABLE) continue;
      if (driver.rideType !== rideType) continue;
      if (!driver.location) continue;

      const dist = driver.location.distanceTo(pickupLocation);
      if (dist <= this.maxSearchRadiusKm && dist < minDistance) {
        minDistance = dist;
        nearest = driver;
      }
    }

    return nearest;
  }

  // ── Trip lifecycle ────────────────────────────────────

  requestTrip(riderId, pickup, drop, rideType) {
    const rider = this.riders.get(riderId);
    if (!rider) throw new Error(`Rider ${riderId} not found.`);

    const trip = new Trip(rider, pickup, drop, rideType);

    // Find nearest driver
    const driver = this.findNearestDriver(pickup, rideType);
    if (!driver) {
      trip.status = TripStatus.CANCELLED;
      console.log(`[TRIP] ${trip.id} - No drivers available. Cancelled.`);
      return trip;
    }

    // Assign driver (auto-accept for simplicity; in real system driver gets a notification)
    trip.driver = driver;
    this.trips.set(trip.id, trip);

    console.log(
      `[TRIP] ${trip.id} requested by ${rider.name}. ` +
      `Matched with ${driver.name}. Distance: ${trip.distance.toFixed(2)} km`
    );

    return trip;
  }

  acceptTrip(driverId, tripId) {
    const trip = this.trips.get(tripId);
    if (!trip) throw new Error(`Trip ${tripId} not found.`);
    if (trip.status !== TripStatus.REQUESTED) {
      throw new Error(`Trip is ${trip.status}, cannot accept.`);
    }

    const driver = this.drivers.get(driverId);
    if (!driver) throw new Error(`Driver ${driverId} not found.`);
    if (trip.driver.id !== driverId) {
      throw new Error('This trip is not assigned to you.');
    }

    trip.status = TripStatus.ACCEPTED;
    driver.status = DriverStatus.ON_TRIP;

    console.log(`[TRIP] ${trip.id} ACCEPTED by ${driver.name}`);
  }

  startTrip(tripId) {
    const trip = this.trips.get(tripId);
    if (!trip) throw new Error(`Trip ${tripId} not found.`);
    if (trip.status !== TripStatus.ACCEPTED) {
      throw new Error(`Trip is ${trip.status}, cannot start.`);
    }

    trip.status = TripStatus.IN_PROGRESS;
    trip.startedAt = new Date();

    console.log(`[TRIP] ${trip.id} IN_PROGRESS. Rider: ${trip.rider.name}`);
  }

  completeTrip(tripId) {
    const trip = this.trips.get(tripId);
    if (!trip) throw new Error(`Trip ${tripId} not found.`);
    if (trip.status !== TripStatus.IN_PROGRESS) {
      throw new Error(`Trip is ${trip.status}, cannot complete.`);
    }

    trip.status = TripStatus.COMPLETED;
    trip.completedAt = new Date();

    // Calculate fare
    trip.fare = this.fareCalculator.calculate(trip);

    // Update driver status
    trip.driver.status = DriverStatus.AVAILABLE;

    // Add to trip history
    trip.rider.tripHistory.push(trip);
    trip.driver.tripHistory.push(trip);

    console.log(
      `[TRIP] ${trip.id} COMPLETED. ` +
      `Distance: ${trip.distance.toFixed(2)} km | Fare: $${trip.fare}`
    );

    return {
      tripId: trip.id,
      fare: trip.fare,
      distance: parseFloat(trip.distance.toFixed(2)),
      durationMin: trip.startedAt
        ? parseFloat(((trip.completedAt - trip.startedAt) / 60000).toFixed(2))
        : 0,
    };
  }

  cancelTrip(tripId) {
    const trip = this.trips.get(tripId);
    if (!trip) throw new Error(`Trip ${tripId} not found.`);
    if (trip.status === TripStatus.COMPLETED || trip.status === TripStatus.CANCELLED) {
      throw new Error(`Trip is already ${trip.status}.`);
    }

    trip.status = TripStatus.CANCELLED;

    if (trip.driver) {
      trip.driver.status = DriverStatus.AVAILABLE;
    }

    console.log(`[TRIP] ${trip.id} CANCELLED`);
  }

  // ── Ratings ───────────────────────────────────────────

  rateTripByRider(tripId, score) {
    const trip = this.trips.get(tripId);
    if (!trip) throw new Error(`Trip ${tripId} not found.`);
    if (trip.status !== TripStatus.COMPLETED) {
      throw new Error('Can only rate completed trips.');
    }
    if (trip.riderRating !== null) {
      throw new Error('Rider has already rated this trip.');
    }

    trip.riderRating = score;
    trip.driver.rating.addRating(score);

    console.log(
      `[RATING] ${trip.rider.name} rated ${trip.driver.name}: ${score}/5 ` +
      `(avg: ${trip.driver.rating.getAverage()})`
    );
  }

  rateTripByDriver(tripId, score) {
    const trip = this.trips.get(tripId);
    if (!trip) throw new Error(`Trip ${tripId} not found.`);
    if (trip.status !== TripStatus.COMPLETED) {
      throw new Error('Can only rate completed trips.');
    }
    if (trip.driverRating !== null) {
      throw new Error('Driver has already rated this trip.');
    }

    trip.driverRating = score;
    trip.rider.rating.addRating(score);

    console.log(
      `[RATING] ${trip.driver.name} rated ${trip.rider.name}: ${score}/5 ` +
      `(avg: ${trip.rider.rating.getAverage()})`
    );
  }

  // ── Fare strategy ─────────────────────────────────────

  setFareCalculator(calculator) {
    this.fareCalculator = calculator;
  }

  // ── Queries ───────────────────────────────────────────

  getRiderHistory(riderId) {
    const rider = this.riders.get(riderId);
    return rider ? rider.tripHistory : [];
  }

  getDriverHistory(driverId) {
    const driver = this.drivers.get(driverId);
    return driver ? driver.tripHistory : [];
  }
}

Usage Walkthrough

// ─── Demo ───────────────────────────────────────────────

const service = new RideService();

// Register riders and drivers
const alice = service.registerRider('Alice', '111-0001');
const bob = service.registerRider('Bob', '111-0002');

const driverRaj = service.registerDriver('Raj', '222-0001', 'KA01AB1234', RideType.ECONOMY);
const driverPriya = service.registerDriver('Priya', '222-0002', 'KA01CD5678', RideType.PREMIUM);
const driverVik = service.registerDriver('Vik', '222-0003', 'KA01EF9012', RideType.ECONOMY);

// Drivers go online and set locations
driverRaj.goOnline();
driverRaj.setLocation(new Location(12.9716, 77.5946));   // Bangalore center

driverPriya.goOnline();
driverPriya.setLocation(new Location(12.9750, 77.5900)); // Nearby

driverVik.goOnline();
driverVik.setLocation(new Location(12.9800, 77.6000));   // Slightly farther

// ── Trip 1: Alice requests economy ride ─────────────────

const pickup = new Location(12.9720, 77.5950);  // Near Raj
const drop = new Location(12.9350, 77.6150);    // ~5 km away

const trip1 = service.requestTrip(alice.id, pickup, drop, RideType.ECONOMY);
// Matched with Raj (nearest ECONOMY driver)

service.acceptTrip(driverRaj.id, trip1.id);
// [TRIP] TRIP-1 ACCEPTED by Raj

service.startTrip(trip1.id);
// [TRIP] TRIP-1 IN_PROGRESS

// ... some time passes ...

const result = service.completeTrip(trip1.id);
// [TRIP] TRIP-1 COMPLETED. Distance: 4.85 km | Fare: $68.80
console.log(result);

// Ratings
service.rateTripByRider(trip1.id, 5);
// Alice rated Raj: 5/5

service.rateTripByDriver(trip1.id, 4);
// Raj rated Alice: 4/5

// ── Trip 2: Bob requests premium ride ───────────────────

const trip2 = service.requestTrip(
  bob.id,
  new Location(12.9730, 77.5940),
  new Location(12.9500, 77.6300),
  RideType.PREMIUM
);
// Matched with Priya (only PREMIUM driver)

service.acceptTrip(driverPriya.id, trip2.id);
service.startTrip(trip2.id);
const result2 = service.completeTrip(trip2.id);
console.log(result2);

// ── Enable surge pricing ────────────────────────────────

service.setFareCalculator(new SurgeFareCalculator(2.0));

const trip3 = service.requestTrip(
  alice.id,
  new Location(12.9720, 77.5950),
  new Location(12.9350, 77.6150),
  RideType.ECONOMY
);
// Raj is now AVAILABLE again
service.acceptTrip(driverRaj.id, trip3.id);
service.startTrip(trip3.id);
const result3 = service.completeTrip(trip3.id);
console.log('Surge fare:', result3.fare); // ~2x the normal fare

// ── Trip history ────────────────────────────────────────

console.log('Alice trips:', service.getRiderHistory(alice.id).length);   // 2
console.log('Raj trips:', service.getDriverHistory(driverRaj.id).length); // 2
console.log('Raj rating:', driverRaj.rating.getAverage());               // 5.0

Edge Cases

Edge CaseHow It Is Handled
No drivers available in radiusrequestTrip() sets trip to CANCELLED
Driver goes offline during matchingfindNearestDriver() checks AVAILABLE status
Driver tries to accept wrong tripacceptTrip() validates driver assignment
Cancel after trip startedcancelTrip() works for any non-completed/non-cancelled trip
Rating outside 1-5 rangeRatingTracker.addRating() throws error
Double rating by same partyTrip checks if riderRating/driverRating is already set
Zero-distance tripFare is just the base fare (perKm component is 0)
Driver goes offline while ON_TRIPgoOffline() throws error

Key Takeaways

  1. Driver matching is the core algorithm: find the nearest available driver of the right type within a radius. In production, this uses spatial indexes (R-trees, geohashing).
  2. Trip lifecycle is a state machine: REQUESTED -> ACCEPTED -> IN_PROGRESS -> COMPLETED, with CANCELLED as an escape state from any pre-completed state.
  3. Strategy Pattern for fare calculation makes it trivial to add surge pricing, promotional discounts, or subscription-based pricing.
  4. Rating system uses a simple running average tracker. In production, you might use weighted averages (recent trips count more).
  5. Location uses the Haversine formula for real distance calculation — interviewers appreciate this level of detail.
  6. The RideService orchestrates the entire flow, keeping Rider, Driver, and Trip as relatively simple data holders with minimal logic.

Next -> 9.6.f — Elevator System