LLD for Ride Booking App: Designing Uber/Lyft
The Matchmaking Problem Behind Every Ride Request
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
Stage 1 — Core Entities and Their Relationships
Rider → requests a trip, has a current location.
Driver → has availability status, current location, vehicle info.
Deep Dive
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
