Low-Level Design: Ride-Sharing Driver App (State Machine, Earnings, Location)

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

Scroll to Top