Low-Level Design: Ride-Sharing Driver App
The driver-side of a ride-sharing app manages driver state, trip offers, earnings tracking, and location reporting. It is a stateful LLD covering state machines, observer pattern, and earnings aggregation.
Core Entities
from dataclasses import dataclass, field
from enum import Enum
from datetime import datetime, date
from typing import Optional, Callable
import uuid
class DriverStatus(Enum):
OFFLINE = "offline"
AVAILABLE = "available"
OFFER_PENDING = "offer_pending" # received trip offer, deciding
EN_ROUTE_PICKUP = "en_route_pickup"
WAITING_FOR_RIDER = "waiting_for_rider"
ON_TRIP = "on_trip"
class TripOfferDecision(Enum):
ACCEPTED = "accepted"
DECLINED = "declined"
EXPIRED = "expired"
@dataclass
class Location:
lat: float
lng: float
recorded_at: datetime = field(default_factory=datetime.utcnow)
@dataclass
class TripOffer:
offer_id: str
trip_id: str
pickup_location: Location
destination_location: Location
estimated_distance_km: float
estimated_fare_cents: int
expires_at: datetime
@property
def is_expired(self) -> bool:
return datetime.utcnow() > self.expires_at
@dataclass
class CompletedTrip:
trip_id: str
rider_id: str
pickup: Location
destination: Location
start_time: datetime
end_time: datetime
distance_km: float
fare_cents: int
tip_cents: int = 0
rating: Optional[int] = None # 1-5
@property
def earnings_cents(self) -> int:
# Driver gets 80% of fare + 100% of tip
return int(self.fare_cents * 0.80) + self.tip_cents
@dataclass
class Driver:
driver_id: str
name: str
license_plate: str
vehicle_model: str
rating: float = 5.0
total_trips: int = 0
acceptance_rate: float = 1.0
status: DriverStatus = DriverStatus.OFFLINE
current_location: Optional[Location] = None
Driver State Machine
class DriverApp:
VALID_TRANSITIONS = {
DriverStatus.OFFLINE: {DriverStatus.AVAILABLE},
DriverStatus.AVAILABLE: {DriverStatus.OFFER_PENDING, DriverStatus.OFFLINE},
DriverStatus.OFFER_PENDING: {DriverStatus.EN_ROUTE_PICKUP, DriverStatus.AVAILABLE},
DriverStatus.EN_ROUTE_PICKUP: {DriverStatus.WAITING_FOR_RIDER},
DriverStatus.WAITING_FOR_RIDER:{DriverStatus.ON_TRIP},
DriverStatus.ON_TRIP: {DriverStatus.AVAILABLE},
}
def __init__(self, driver: Driver):
self.driver = driver
self.current_offer: Optional[TripOffer] = None
self.current_trip_id: Optional[str] = None
self._observers: list[Callable] = []
def add_observer(self, callback: Callable) -> None:
self._observers.append(callback)
def _notify(self, event: str, data: dict) -> None:
for obs in self._observers:
obs(event, data)
def _transition(self, new_status: DriverStatus) -> None:
allowed = self.VALID_TRANSITIONS.get(self.driver.status, set())
if new_status not in allowed:
raise ValueError(
f"Cannot transition {self.driver.status} -> {new_status}"
)
old = self.driver.status
self.driver.status = new_status
self._notify("status_changed", {"from": old, "to": new_status,
"driver_id": self.driver.driver_id})
def go_online(self) -> None:
self._transition(DriverStatus.AVAILABLE)
def go_offline(self) -> None:
self._transition(DriverStatus.OFFLINE)
def receive_offer(self, offer: TripOffer) -> None:
self._transition(DriverStatus.OFFER_PENDING)
self.current_offer = offer
self._notify("offer_received", {"offer_id": offer.offer_id,
"fare_cents": offer.estimated_fare_cents})
def respond_to_offer(self, decision: TripOfferDecision) -> None:
if not self.current_offer:
raise ValueError("No pending offer")
if self.current_offer.is_expired:
decision = TripOfferDecision.EXPIRED
if decision == TripOfferDecision.ACCEPTED:
self._transition(DriverStatus.EN_ROUTE_PICKUP)
self.current_trip_id = self.current_offer.trip_id
self._notify("offer_accepted", {"trip_id": self.current_trip_id})
else:
self._transition(DriverStatus.AVAILABLE)
# Update acceptance rate
total = self.driver.total_trips + 1
self.driver.acceptance_rate = (
(self.driver.acceptance_rate * (total - 1) + 0) / total
)
self._notify("offer_declined", {"reason": decision.value})
self.current_offer = None
def arrived_at_pickup(self) -> None:
self._transition(DriverStatus.WAITING_FOR_RIDER)
def start_trip(self) -> None:
self._transition(DriverStatus.ON_TRIP)
self._notify("trip_started", {"trip_id": self.current_trip_id})
def complete_trip(self, fare_cents: int, distance_km: float) -> CompletedTrip:
trip = CompletedTrip(
trip_id=self.current_trip_id,
rider_id="", # filled by system
pickup=self.driver.current_location,
destination=self.driver.current_location,
start_time=datetime.utcnow(),
end_time=datetime.utcnow(),
distance_km=distance_km,
fare_cents=fare_cents,
)
self.driver.total_trips += 1
self.driver.acceptance_rate = (
(self.driver.acceptance_rate * (self.driver.total_trips - 1) + 1)
/ self.driver.total_trips
)
self.current_trip_id = None
self._transition(DriverStatus.AVAILABLE)
self._notify("trip_completed", {"trip_id": trip.trip_id,
"earnings": trip.earnings_cents})
return trip
Earnings Tracker
from collections import defaultdict
class EarningsTracker:
def __init__(self, driver_id: str):
self.driver_id = driver_id
self._daily: dict[date, int] = defaultdict(int) # cents
self._weekly: dict[int, int] = defaultdict(int) # week_number -> cents
self._trips: list[CompletedTrip] = []
def record_trip(self, trip: CompletedTrip) -> None:
self._trips.append(trip)
trip_date = trip.end_time.date()
week = trip_date.isocalendar()[1]
self._daily[trip_date] += trip.earnings_cents
self._weekly[week] += trip.earnings_cents
def daily_earnings(self, day: date = None) -> int:
return self._daily[day or date.today()]
def weekly_earnings(self, week: int = None) -> int:
if week is None:
week = date.today().isocalendar()[1]
return self._weekly[week]
def average_earnings_per_trip(self) -> float:
if not self._trips:
return 0.0
total = sum(t.earnings_cents for t in self._trips)
return total / len(self._trips)
def trips_today(self) -> int:
today = date.today()
return sum(1 for t in self._trips if t.end_time.date() == today)
Location Reporting
import threading
import time
class LocationReporter:
"""Background thread updating driver location every 4 seconds when online."""
def __init__(self, driver_app: DriverApp, location_service):
self.app = driver_app
self.location_service = location_service
self._running = False
self._thread: Optional[threading.Thread] = None
def start(self) -> None:
self._running = True
self._thread = threading.Thread(target=self._report_loop, daemon=True)
self._thread.start()
def stop(self) -> None:
self._running = False
def _report_loop(self) -> None:
while self._running:
if self.app.driver.status != DriverStatus.OFFLINE:
loc = self._get_current_gps()
self.app.driver.current_location = loc
self.location_service.update(
self.app.driver.driver_id,
loc.lat, loc.lng,
self.app.driver.status.value,
)
time.sleep(4)
def _get_current_gps(self) -> Location:
# In production: read from device GPS sensor
return Location(lat=37.7749, lng=-122.4194) # placeholder
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What design patterns are used in a ride-sharing driver app?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “State machine for driver status (OFFLINE/AVAILABLE/OFFER_PENDING/EN_ROUTE_PICKUP/ON_TRIP) u2014 VALID_TRANSITIONS dict prevents illegal status changes. Observer pattern for event notifications (status change u2192 update UI, location service, analytics) without coupling business logic to display code. Strategy pattern for location update frequency (aggressive while ON_TRIP, conservative while AVAILABLE). Template method for trip lifecycle (receive_offer u2192 respond u2192 en_route u2192 start_trip u2192 complete_trip).”
}
},
{
“@type”: “Question”,
“name”: “How do you handle an expired trip offer?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Store offer.expires_at when the offer arrives. When respond_to_offer() is called, check offer.is_expired before processing the decision. If expired, treat as DECLINED regardless of the driver’s choice. Transition back to AVAILABLE. Update acceptance_rate as a non-acceptance (penalize or ignore depending on business rules u2014 expired offers due to app freeze might be forgiven). In production, the server also enforces expiry: if it does not receive a response within 15 seconds, it marks the offer expired and retries with another driver.”
}
},
{
“@type”: “Question”,
“name”: “How do you track a driver’s earnings accurately?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Record each CompletedTrip with fare_cents and tip_cents. Driver earnings = fare * platform_payout_rate (typically 75-80%) + tip (100%). Aggregate to daily and weekly totals using date keys. For accuracy: never compute earnings from the total fare alone u2014 store the breakdown (fare, tip, bonuses) separately and apply rates per component. Track week by ISO week number for consistent weekly summaries. In production, earnings are authoritative on the server; the local app shows an estimate that reconciles on trip completion.”
}
},
{
“@type”: “Question”,
“name”: “Why does the driver app report location every 4 seconds?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “4 seconds balances real-time tracking accuracy against battery drain and data usage. For the matching service: driver positions older than 30 seconds are unreliable u2014 4s intervals keep data fresh. For the rider: ETA updates with smooth map animation require positions every few seconds. Battery trade-off: GPS + cellular radio every 4s consumes ~5-10% battery per hour. Adaptive reporting: some apps slow to every 10s when driver is parked or idle (speed < 5 km/h) and return to 4s when moving. Uber uses a variable 1-4s interval based on movement speed."
}
},
{
"@type": "Question",
"name": "How do you calculate a driver's acceptance rate accurately?",
"acceptedAnswer": {
"@type": "Answer",
"text": "acceptance_rate = accepted_offers / total_offers_received. Update incrementally: on each offer response, total_offers += 1; if accepted, accepted_offers += 1; rate = accepted_offers / total_offers. For a rolling window (last 30 days), store daily counts and sum. Considerations: expired offers (driver did not respond in time) count as non-accepted in Uber's system. Cancellations after acceptance may or may not count against rate depending on whether the driver or the rider cancelled. Acceptance rate below a threshold (e.g., 80%) can limit which promotions a driver qualifies for."
}
}
]
}
Asked at: Uber Interview Guide
Asked at: Lyft Interview Guide
Asked at: DoorDash Interview Guide
Asked at: Snap Interview Guide