Core Entities
Rider: rider_id, name, phone, email, payment_method_id, rating, total_trips. Driver: driver_id, name, phone, license_number, vehicle_id, status (OFFLINE, AVAILABLE, ON_TRIP), current_lat, current_lng, rating, acceptance_rate. Vehicle: vehicle_id, driver_id, make, model, year, license_plate, type (ECONOMY, COMFORT, XL, BLACK). Trip: trip_id, rider_id, driver_id, vehicle_id, status, pickup_lat, pickup_lng, dropoff_lat, dropoff_lng, requested_at, accepted_at, picked_up_at, completed_at, base_fare, surge_multiplier, total_fare, distance_meters, duration_seconds. TripEvent: event_id, trip_id, event_type, timestamp, lat, lng (GPS breadcrumbs).
Driver Matching Algorithm
class MatchingService:
def find_driver(self, rider_lat: float, rider_lng: float,
vehicle_type: str) -> Optional[Driver]:
# Find available drivers within 5km using Redis geo index
nearby = self.geo.find_nearby_drivers(
lat=rider_lat, lng=rider_lng,
radius_km=5.0,
vehicle_type=vehicle_type
)
if not nearby:
return None
def score(d: Driver) -> float:
distance = haversine(rider_lat, rider_lng, d.lat, d.lng)
eta_minutes = distance / AVG_SPEED_KM_H * 60
# Lower score = better: prefer closer, higher-rated drivers
return eta_minutes * 0.7 + (5 - d.rating) * 0.3
candidates = sorted(nearby[:20], key=score)
for driver in candidates:
# Offer the trip; driver has 15 seconds to accept
accepted = self.offer_trip(driver, timeout=15)
if accepted:
return driver
return None # no driver accepted
Driver location is stored in a Redis geo index: GEOADD driver_locations lng lat driver_id. Updated every 4 seconds from the driver app. GEORADIUS queries find all drivers within the radius in O(log n + k) time. The scoring function balances proximity (ETA) and driver quality (rating). Offer-then-retry: always try the best candidate first, then fall back — this gives good drivers more trip opportunities (improving retention) while ensuring the rider gets matched quickly.
Trip State Machine
Valid states and transitions: REQUESTED → ACCEPTED (driver accepts within 15s) or CANCELLED (timeout or rider cancels). ACCEPTED → EN_ROUTE_TO_PICKUP (driver heads to pickup). EN_ROUTE_TO_PICKUP → ARRIVED_AT_PICKUP (driver arrives at pickup location). ARRIVED_AT_PICKUP → IN_PROGRESS (rider boards). IN_PROGRESS → COMPLETED (driver ends trip at destination) or CANCELLED (rare, with special handling). Each transition stores a timestamp. Invalid transitions are rejected. The state machine is enforced in the TripService: before any state transition, verify the current state is valid for the attempted transition.
Surge Pricing
Surge pricing balances supply and demand in real time. Surge multiplier = f(demand / supply) in a geographic zone. Zones: divide the city into H3 hexagonal cells (Uber uses H3 library). For each cell: count active trip requests (demand) and available drivers (supply) in the last 5 minutes. Multiplier = max(1.0, demand / (supply * TARGET_UTILIZATION)). Cap at 5.0x to prevent extreme pricing. Implementation: a pricing job runs every 60 seconds, recomputes multipliers per cell, stores results in Redis with a 2-minute TTL. On trip request: look up the rider’s cell and destination cell, take the max multiplier. Display the surge multiplier prominently in the app before the rider confirms (regulatory requirement in many markets). Store surge_multiplier on the Trip at the time of request — the rider is not charged more if surge increases during the trip.
Fare Calculation
Base fare = base_amount + (per_minute_rate * duration) + (per_km_rate * distance). Surge fare = base_fare * surge_multiplier. Service fee = fixed percentage (e.g., 25% platform commission). Rider pays: surge_fare + service_fee + tip (optional). Driver earns: surge_fare * (1 – commission_rate). Minimum fare: max(calculated_fare, minimum_fare_for_vehicle_type). Cancellation fee: if the driver has waited > 5 minutes at pickup and the rider cancels, charge the rider a cancellation fee. Special pricing: airport flat rate, tolls (pass-through to rider), peak-hour rate per city rules.
. Before any state update: assert current_state in VALID_TRANSITIONS and new_state in VALID_TRANSITIONS[current_state]. Store the transitions with timestamps in TripEvents for the customer receipt and for dispute resolution. Atomic transition: UPDATE trips SET status = new_state, transition_at = NOW() WHERE trip_id = :id AND status = :current_state. If rows_affected = 0: a concurrent update changed the status — retry or return a conflict error. This optimistic check prevents concurrent state updates from putting the trip in an inconsistent state.”
}
},
{
“@type”: “Question”,
“name”: “How do you calculate the fare and split it between the driver and platform?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Fare components: base_fare (flat pickup fee, e.g., $1.00) + time_fare (per_minute_rate * trip_duration_minutes) + distance_fare (per_km_rate * trip_distance_km). Surge multiplier applied to the total: total_fare = (base_fare + time_fare + distance_fare) * surge_multiplier. Minimum fare enforced: max(total_fare, minimum_fare). Service fee (platform commission): platform_fee = total_fare * commission_rate (e.g., 25%). Rider pays: total_fare + tip. Driver earns: total_fare – platform_fee + tip = total_fare * (1 – commission_rate) + tip. All amounts stored as integers (cents) to avoid floating-point errors. Tax: in many jurisdictions, sales tax or GST applies to the fare — itemized separately on the receipt. Distance and duration are computed from GPS breadcrumbs (TripEvents) rather than the routing API estimate, to account for real-world deviations (traffic, route changes).”
}
},
{
“@type”: “Question”,
“name”: “How do you handle driver GPS location updates at scale?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Drivers send GPS pings every 4 seconds from the mobile app. At 1 million active drivers: 250,000 location updates/second. Architecture: driver app u2192 location service (REST or gRPC) u2192 Redis GEOADD + Kafka publish. Redis GEOADD updates the driver’s position in the geo index (O(log n) per update). Kafka receives the raw event for downstream consumers: trip tracking (update rider’s map view), analytics (heat maps, demand forecasting), and compliance logging (GPS trail for insurance/dispute). Redis geo index stores only available drivers (status=AVAILABLE). Drivers on trips are removed from the index (no point in routing trips to them). On trip completion: re-add the driver to the Redis geo index with their current position. Memory: 1M drivers * ~50 bytes per geo entry = 50MB — easily fits in Redis.”
}
}
]
}
Asked at: Uber Interview Guide
Asked at: Lyft Interview Guide
Asked at: DoorDash Interview Guide
Asked at: Snap Interview Guide