Back to System Design Problems
Q8Question 8 of 13

Design a Ride-Sharing System (like Uber/Ola)

Key Concepts
  • Drivers send GPS coordinates every few seconds; a geospatial index (geohash, Redis GEO) enables fast nearby-driver queries.
  • Matching picks the nearest available driver by shortest ETA, with an atomic claim to prevent double-assignment.
  • ETA uses a routing engine over the road network with live traffic, not straight-line distance.
  • Surge pricing multiplies the fare based on the ratio of open requests to available drivers in an area.
  • A strict trip state machine (REQUESTED -> MATCHED -> ARRIVED -> IN_PROGRESS -> COMPLETED) prevents illegal transitions.
  • The driver claim must be atomic so one driver is never offered to two riders simultaneously.
  • Location data is best-effort (tolerate missed pings), but matching and billing require strict correctness.

Definition

Index driver locations geospatially for fast nearest-driver matching, estimate arrival with routing, price by live supply and demand, and move every trip through a strict state machine.
  • Geospatial index : encodes locations for fast nearby queries (geohash, Redis GEO commands).
  • Driver matching : picks the best available driver by shortest ETA with an atomic claim.
  • Surge pricing : dynamic fare multiplier based on the ratio of open requests to available drivers.
  • Trip state machine : enforces legal transitions (REQUESTED -> MATCHED -> ARRIVED -> IN_PROGRESS -> COMPLETED).
  • Atomic claim : marks a driver as busy in one indivisible operation so two riders cannot be promised the same car.

National Bank of India runs a fleet of cash-delivery vans. The dispatch system always knows where every van is on a live map, finds the closest free van, tells the branch it will arrive in eight minutes, charges more when every van is busy during a rush, and tracks the delivery through stages. Replace vans with cars and branches with riders.

Understanding the Concepts

How does location tracking work?

Each driver's app sends GPS coordinates every few seconds. A geospatial index like geohash or Redis GEO enables fast radius queries. At National Bank of India's cash-van dispatch, every van broadcasts its position so the system always has a live view of the fleet.

JAVA
package com.bank.rideshare;

import java.util.List;

public class LocationService {
    public void updateLocation(String driverId, double lat, double lng) {
        geoIndex.update(driverId, lat, lng);
    }
    public List<String> nearbyDrivers(double lat, double lng, double radiusKm) {
        return geoIndex.nearby(lat, lng, radiusKm);
    }
}

How does driver matching work?

Pick the nearest available driver by shortest ETA. The claim must be atomic so two riders never get the same driver. The system iterates through nearby drivers in order of proximity and claims the first available one.

JAVA
package com.bank.rideshare;

public class MatchingService {
    public String match(double riderLat, double riderLng) {
        var nearby = locationService.nearbyDrivers(riderLat, riderLng, 5.0);
        for (String driverId : nearby) {
            if (driverStore.claimAtomic(driverId)) return driverId;  // atomic!
        }
        throw new NoDriverAvailableException();
    }
}

How does surge pricing work?

Surge multiplier is derived from the ratio of open requests to available drivers in a geographic cell. When demand exceeds supply, the fare multiplier rises to discourage marginal demand and attract more drivers. The multiplier is capped to avoid extreme fares.

JAVA
package com.bank.rideshare;

import java.math.BigDecimal;

public class SurgePricing {
    public BigDecimal multiplier(int openRequests, int availableDrivers) {
        if (availableDrivers == 0) return new BigDecimal("3.0");
        double ratio = (double) openRequests / availableDrivers;
        if (ratio <= 1.0) return BigDecimal.ONE;
        return BigDecimal.valueOf(Math.min(3.0, 1.0 + (ratio - 1.0)));
    }
}

How does the trip state machine work?

The state machine enforces legal transitions. Each transition triggers side effects like notifications and payment. An illegal transition (e.g., jumping from REQUESTED to COMPLETED) is rejected. This prevents bugs where a trip is billed without the driver ever arriving.

JAVA
package com.bank.rideshare;

import java.util.Map;
import java.util.Set;

public class TripStateMachine {
    private static final Map<TripState, Set<TripState>> ALLOWED = Map.of(
        TripState.REQUESTED,   Set.of(TripState.MATCHED, TripState.CANCELLED),
        TripState.MATCHED,     Set.of(TripState.ARRIVED, TripState.CANCELLED),
        TripState.ARRIVED,     Set.of(TripState.IN_PROGRESS),
        TripState.IN_PROGRESS, Set.of(TripState.COMPLETED)
    );

    public void transition(Trip trip, TripState newState) {
        if (!ALLOWED.getOrDefault(trip.state(), Set.of()).contains(newState))
            throw new IllegalStateTransitionException(trip.state(), newState);
        trip.setState(newState);
        sideEffects.trigger(trip, newState);  // notify, bill, rate
    }
}

How does ETA calculation work?

Use a routing engine over the road network with live traffic data, not straight-line distance. The ETA considers road segments, speed limits, current congestion, and turn costs. A straight-line distance of 2 km might be a 15-minute drive in heavy traffic.

JAVA
package com.bank.rideshare;

public class ETAService {
    public int estimateMinutes(double fromLat, double fromLng, double toLat, double toLng) {
        Route route = routingEngine.findFastest(fromLat, fromLng, toLat, toLng);
        return (int) Math.ceil(route.durationSeconds() / 60.0);
    }
}

How do you handle cancellation?

Cancellation is allowed from REQUESTED and MATCHED states with no fee. Late cancellation (after the driver has arrived) incurs a fee to compensate the driver's time and fuel. The state machine enforces which states allow cancellation.

What about fare calculation?

The fare combines base fare, per-km rate, per-minute rate, and the surge multiplier. All amounts use BigDecimal to avoid floating-point errors in billing. The final fare is computed after trip completion using the actual route distance and duration.

JAVA
package com.bank.rideshare;

import java.math.BigDecimal;

public class FareCalculator {
    public BigDecimal calculate(double distanceKm, int durationMin, BigDecimal surgeMultiplier) {
        BigDecimal base = new BigDecimal("50.00");
        BigDecimal perKm = BigDecimal.valueOf(distanceKm).multiply(new BigDecimal("12.00"));
        BigDecimal perMin = BigDecimal.valueOf(durationMin).multiply(new BigDecimal("2.00"));
        return base.add(perKm).add(perMin).multiply(surgeMultiplier);
    }
}

What about data consistency?

Location data is best-effort: tolerate missed GPS pings and stale positions. But driver claims and billing must be atomic and correct. Separating these two consistency requirements is the key architectural decision.

What should a senior engineer know?

A senior engineer separates the best-effort location pipeline from the correctness-critical matching and billing paths, tolerating missed GPS pings without failing trips. They also design geospatial sharding by region, handle cross-region trips gracefully, and plan for driver incentive programs that adjust surge thresholds dynamically.

Visual Overview

DIAGRAM
  Ride-Sharing Architecture

  ┌──────────┐  GPS every 3s  ┌──────────────┐
  │ Driver   │───────────────►│ Geo Index    │
  │ App      │                │ (Redis GEO)  │
  └──────────┘                └──────┬───────┘
                                     │ nearby query
  ┌──────────┐  ride request  ┌──────▼───────┐
  │ Rider    │───────────────►│ Matching     │
  │ App      │                │ Service      │
  └──────────┘                │ (atomic claim│
                              │  on driver)  │
                              └──────┬───────┘
                                     │
  Trip State Machine:
  REQUESTED ──► MATCHED ──► ARRIVED ──► IN_PROGRESS ──► COMPLETED
       │            │                                       │
       └── CANCELLED └── CANCELLED                    Payment + Rating

  Surge pricing:
  open_requests / available_drivers = ratio
  ratio <= 1.0 -> 1.0x (no surge)
  ratio  > 1.0 -> min(3.0, 1 + ratio - 1) surge

Key Takeaway

A ride-sharing system is real-time and location-heavy, built around geospatial indexing, atomic driver matching, routing-based ETA, surge pricing, and a strict trip state machine. The atomic driver claim is the correctness linchpin: one driver must never be offered to two riders.

Location data is best-effort (tolerate missed pings), but matching and billing demand strict correctness. Separating these concerns is what keeps the platform both responsive and trustworthy.

Remember one sentence: index driver locations geospatially, match with an atomic claim, price by live supply-demand ratio, and enforce a strict trip state machine from request to completion.

---

Share
Use ← → arrow keys to navigate