Skip to main content
Articles/design patterns/low-level-design-guide-for-ride-booking-application
Series: Object-Oriented Design Exercises•Part 4 of 5
Scenario Article

LLD for Ride Booking App: Designing Uber/Lyft

The Matchmaking Problem Behind Every Ride Request

Abstract Algorithms•Feb 11, 2026
💡 Executive Summary

A ride-booking system (Uber/Lyft-style) needs three interleaved sub-systems: real-time driver location tracking (Observer Pattern), nearest-driver matching (geospatial query), and dynamic pricing (Strategy Pattern).

🎯 System Scenario & Requirements

System Goal

A ride-booking system (Uber/Lyft-style) needs three interleaved sub-systems: real-time driver location tracking (Observer Pattern), nearest-driver matching (geospatial query), and dynamic pricing (Strategy Pattern).

Actors & Components

  • Mobile Real-time Clients
  • Geospatial & Matching
  • Location State & Dispatches
  • Driver Mobile App
  • Passenger App
  • WebSocket Gateway
  • Location Service
  • Matching Engine

Functional Requirements

  • TLDR: A ride-booking system (Uber/Lyft-style) needs three interleaved sub-systems: real-time driver location tracking (Observer Pattern), nearest-driver matching (geospatial query), and dynamic pricing (Strategy Pattern…
  • Getting state transitions right — Requested → Accepted → InProgress → Completed — is the core LLD challenge.
  • When a rider taps "Book Ride," a deceptively complex chain of decisions fires within seconds: 1.

Constraints & Assumptions

⚡ Execution Stages & Decision Trace

1

Stage 1 — Core Entities and Their Relationships

Action / Decision:

Rider → requests a trip, has a current location.

Component Behavior:

Driver → has availability status, current location, vehicle info.

Observable Output: Trip → ties a rider to a driver, from pickup to drop-off.
Read Full Stage in Deep Dive ↓
📖

Deep Dive

Detailed Architectural Analysis

Scenario context

A ride-booking system (Uber/Lyft-style) needs three interleaved sub-systems: real-time driver location tracking (Observer Pattern), nearest-driver matching (geospatial query), and dynamic pricing (Strategy Pattern). Getting state transitions right — Requested → Accepted → InProgress → Completed — is the core LLD challenge.

enum TripStatus {
    REQUESTED, ACCEPTED, IN_PROGRESS, COMPLETED, CANCELLED
}

Observable outcome: TripStatus enum: DriverStatus enum: 📊 Driver Availability State Machine

Stage 2 — State Transitions: The Trip Lifecycle

  • Every transition is triggered by an explicit event.
  • Wrong transitions must be rejected.
stateDiagram-v2
    [*] --> REQUESTED : rider taps Book
    REQUESTED --> ACCEPTED : driver accepts
    REQUESTED --> CANCELLED : no driver found / rider cancels
    ACCEPTED --> IN_PROGRESS : driver arrives at pickup
    IN_PROGRESS --> COMPLETED : destination reached
    IN_PROGRESS --> CANCELLED : rider or driver cancels mid-trip
    COMPLETED --> [*]
    CANCELLED --> [*]
// Transition enforcement
public void transition(TripStatus newStatus) {
    if (!isValidTransition(this.status, newStatus)) {
        throw new IllegalStateException(
            "Cannot transition from " + this.status + " to " + newStatus);
    }
    this.status = newStatus;
}

private boolean isValidTransition(TripStatus from, TripStatus to) {
    return switch (from) {
        case REQUESTED   -> to == ACCEPTED   || to == CANCELLED;
        case ACCEPTED    -> to == IN_PROGRESS || to == CANCELLED;
        case IN_PROGRESS -> to == COMPLETED  || to == CANCELLED;
        default          -> false;
    };
}

Observable outcome: Wrong transitions must be rejected.

Stage 3 — Real-World Applications: Observer Pattern: Real-Time Driver Location Updates

  • Riders, dispatchers, and ETAs all need to react when a driver's location changes.
  • The Observer Pattern decouples location updates from their consumers.
// Observer interface
interface LocationObserver {
    void onLocationUpdate(String driverId, Location newLocation);
}

// Subject (driver location tracker)
class DriverLocationService {
    private Map<String, List<LocationObserver>> subscribers = new HashMap<>();

    public void subscribe(String driverId, LocationObserver observer) {
        subscribers.computeIfAbsent(driverId, k -> new ArrayList<>()).add(observer);
    }

    public void updateLocation(String driverId, Location loc) {
        subscribers.getOrDefault(driverId, List.of())
                   .forEach(obs -> obs.onLocationUpdate(driverId, loc));
    }
}

// Concrete observer
class RiderEtaTracker implements LocationObserver {
    @Override
    public void onLocationUpdate(String driverId, Location loc) {
        // Recalculate ETA and push to rider's app
        System.out.println("ETA updated for driver " + driverId + " at " + loc);
    }
}

Observable outcome: The Observer Pattern decouples location updates from their consumers.

Stage 4 — Matching Algorithm: Finding the Nearest Available Driver

  • The simplest valid matching algorithm: 1.
  • Filter drivers by status = AVAILABLE.
  • For each candidate, compute distance to rider pickup.
  • Return the driver with minimum distance.
graph TD
    A[Ride Request received  pickup location known] --> B[Query driver index: status = AVAILABLE]
    B --> C[For each candidate: compute Haversine distance to pickup]
    C --> D{Any drivers within radius?}
    D -->|No| E[Expand radius or notify rider of no availability]
    D -->|Yes| F[Sort candidates by distance ascending]
    F --> G[Assign nearest driver  send acceptance request]
    G --> H{Driver accepts?}
    H -->|Yes| I[Trip transitions to ACCEPTED]
    H -->|No| F
class MatchingService {
    public Optional<Driver> findNearest(Location pickup, List<Driver> drivers) {
        return drivers.stream()
            .filter(d -> d.getStatus() == DriverStatus.AVAILABLE)
            .min(Comparator.comparingDouble(d -> haversine(pickup, d.getLocation())));
    }

    private double haversine(Location a, Location b) {
        double R = 6371.0;
        double dLat = Math.toRadians(b.lat() - a.lat());
        double dLon = Math.toRadians(b.lon() - a.lon());
        double h = Math.sin(dLat/2)*Math.sin(dLat/2)
                 + Math.cos(Math.toRadians(a.lat()))
                 * Math.cos(Math.toRadians(b.lat()))
                 * Math.sin(dLon/2)*Math.sin(dLon/2);
        return 2 * R * Math.asin(Math.sqrt(h));
    }
}

Observable outcome: Return the driver with minimum distance.

Stage 5 — Strategy Pattern: Flexible Pricing Rules

  • Pricing logic must change at runtime — flat rate, surge, subscription, etc.
  • The Strategy Pattern lets you swap rules without modifying TripPricingEngine .
interface PricingStrategy {
    double calculateFare(double distanceKm, int durationMinutes, double surgeMultiplier);
}

class StandardPricing implements PricingStrategy {
    public double calculateFare(double distKm, int durMin, double surge) {
        return (0.5 + 1.2 * distKm + 0.25 * durMin) * surge;
    }
}

class FlatRatePricing implements PricingStrategy {
    private double flatRate;
    public FlatRatePricing(double rate) { this.flatRate = rate; }
    public double calculateFare(double d, int t, double s) { return flatRate; }
}

class TripPricingEngine {
    private PricingStrategy strategy;
    public TripPricingEngine(PricingStrategy s) { this.strategy = s; }
    public void setStrategy(PricingStrategy s) { this.strategy = s; }
    public double quote(double km, int min, double surge) {
        return strategy.calculateFare(km, min, surge);
    }
}

Observable outcome: The Strategy Pattern lets you swap rules without modifying TripPricingEngine .

Stage 6 — OOP Pillars Applied to the Ride-Booking Domain

  • Every core class in this design maps directly to one of the four OOP pillars.
  • The mapping is explicit, not accidental — each pillar solves a specific design problem in the ride-booking context.
  • Driver.status is private — it is only mutated through named domain operations.
  • PremiumDriver and EvDriver inherit the full base contract and extend it.
classDiagram
    class Driver {
        +String id
        +String name
        -DriverStatus status
        +updateLocation(Location loc)
        +acceptRide(TripRequest req)
        +completeRide(Trip trip)
        +isAvailableFor(TripRequest req) bool
    }
    class PremiumDriver {
        +String vehicleClass
        +bool hasWifi
    }
    class EvDriver {
        +double batteryPercent
        +isAvailableFor(TripRequest req) bool
    }
    class MatchingStrategy {
        <>
        +match(TripRequest req, List~Driver~ available) Optional~Driver~
    }
    class PricingStrategy {
        <>
        +calculate(TripRequest req, Driver driver) Fare
    }
    class TripRequestService {
        -MatchingStrategy matcher
        -PricingStrategy pricer
        +requestRide(TripRequest req) Trip
    }
    class Trip {
        +String tripId
        +TripStatus status
        +transition(TripStatus next)
    }
    PremiumDriver --|> Driver : extends
    EvDriver --|> Driver : extends
    TripRequestService o-- MatchingStrategy : uses (injected)
    TripRequestService o-- PricingStrategy : uses (injected)
    Driver --> Trip : fulfills
classDiagram
    class TripRequest {
        +Location pickup
        +Location dropoff
        +String riderId
    }
    class Trip {
        +String tripId
        -TripStatus status
        +Driver assignedDriver
        +transition(TripStatus) void
    }
    class Driver {
        +String id
        +String name
        -DriverStatus status
        +Location location
        +acceptRide(TripRequest) void
        +completeRide(Trip) void
        +isAvailableFor(TripRequest) boolean
    }
    class PremiumDriver {
        +String vehicleClass
        +boolean hasWifi
    }
    class EvDriver {
        +double batteryPercent
        +isAvailableFor(TripRequest) boolean
    }
    class TripRequestService {
        -MatchingStrategy matcher
        -PricingStrategy pricer
        +requestRide(TripRequest) Trip
    }
    class MatchingStrategy {
        <>
        +match(TripRequest, List~Driver~) Optional~Driver~
    }
    class PricingStrategy {
        <>
        +calculate(TripRequest, Driver) Fare
    }
    class LocationObserver {
        <>
        +onLocationUpdate(String, Location) void
    }
    class DriverLocationService {
        -Map~String,List~ subscribers
        +subscribe(String, LocationObserver) void
        +updateLocation(String, Location) void
    }
    class RiderEtaTracker {
        +onLocationUpdate(String, Location) void
    }
    class TripStatus {
        <>
        REQUESTED
        ACCEPTED
        IN_PROGRESS
        COMPLETED
        CANCELLED
    }
    class DriverStatus {
        <>
        AVAILABLE
        BUSY
        OFFLINE
    }
    PremiumDriver --|> Driver : extends
    EvDriver --|> Driver : extends
    TripRequestService o-- MatchingStrategy : uses (injected)
    TripRequestService o-- PricingStrategy : uses (injected)
    RiderEtaTracker ..|> LocationObserver : implements
    DriverLocationService --> LocationObserver : notifies
    Trip --> TripStatus : state
    Driver --> DriverStatus : state
    Trip --> Driver : assigned to
    TripRequest --> Trip : becomes

Observable outcome: PremiumDriver and EvDriver inherit the full base contract and extend it.

End-to-end outcome

When a rider taps "Book Ride," a deceptively complex chain of decisions fires within seconds: 1.

Alternatives considered

Decision Guide: LLD Decision Guide for Ride Booking

Decision Guide: LLD Decision Guide for Ride Booking

Failure and degraded modes

Trade-offs & Failure Modes: Trade-offs and Production Constraints

  • Detection: Trade-offs & Failure Modes: Trade-offs and Production Constraints
  • User or operator impact: Trade-offs & Failure Modes: Trade-offs and Production Constraints

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit