Design Uber (Ride Sharing)
OOP design for a ride-sharing platform covering rider-driver matching, trip lifecycle management, fare estimation with surge pricing, location tracking with spatial indexing, and rating systems.
The Problem
Your company runs a ride-sharing service in a city with 15,000 active drivers and 200,000 daily ride requests. Right now, the matching logic lives inside a 4,000-line RideController class. When a rider requests a trip, the system loops through every driver in the database, calculates straight-line distance to each one, and picks the closest driver whose status column says "available." Last Saturday evening during a concert surge, 3,000 ride requests hit in a 5-minute window. The linear scan took 2+ seconds per request, drivers were assigned to rides they had already accepted elsewhere, and 1,800 riders waited 20+ minutes with no match.
Ride-sharing systems are among the hardest LLD problems because they combine a rich trip lifecycle, real-time location tracking, dynamic pricing, and a two-sided marketplace. A trip does not simply go from "requested" to "completed." It flows through matching, driver en-route, arrival, in-progress, and completion, with each transition affecting driver availability, fare calculation, and notification dispatch. The matching algorithm must consider driver proximity, current load, estimated arrival time, and ride type. Pricing involves base fares, per-mile rates, per-minute rates, and surge multipliers that change by the minute.
Design the core classes for a ride-sharing platform that handles ride requests, driver matching with spatial indexing, trip state machine transitions, fare estimation with surge pricing, two-way ratings, and cancellation rules.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "What ride types does the platform support? Are pool rides, premium vehicles, or scheduled rides in scope?"
Interviewer: "Support three ride types for now: Economy, Premium, and XL. Pool rides and scheduled rides are out of scope but good extensibility topics."
Three ride types means different vehicle requirements and different fare rates. Economy accepts any standard car, Premium requires luxury vehicles, and XL requires vehicles with 6+ seats.
You: "How does the system find nearby drivers? Does the rider specify a pickup location and the system searches within a radius?"
Interviewer: "Yes. The rider provides a pickup location and the system searches for available drivers within a configurable radius, defaulting to 5 km. If no drivers are found, expand the radius once and then fail."
Radius-based search suggests spatial indexing. A naive loop through all 15,000 drivers is too slow. Grid-based partitioning lets you check only the cells near the pickup point.
You: "How does driver matching work? Just pick the closest, or are there other criteria?"
Interviewer: "Support multiple matching strategies: nearest available driver, highest-rated driver, and best ETA. The platform should be able to switch strategies per ride type or market."
Multiple matching strategies is a clear Strategy pattern signal. Each strategy takes the same input (rider location, available drivers, ride type) and returns a ranked list.
You: "How does surge pricing work? Is it a simple multiplier on the base fare?"
Interviewer: "Yes. Each geographic zone has a surge multiplier that updates periodically based on demand-to-supply ratio. The fare estimate shows the multiplied price before the rider confirms. Minimum multiplier is 1.0x, maximum is 5.0x."
Zone-based surge means each area has its own multiplier. The fare calculator applies this multiplier to the base fare components. Riders see the estimated fare before confirming.
You: "What happens if a rider or driver cancels? Are there cancellation fees?"
Interviewer: "Riders can cancel for free within 2 minutes of requesting. After that, or after the driver has arrived, a cancellation fee applies. Drivers can cancel too, but it affects their rating. Cancellation rules depend on the current trip state."
State-dependent cancellation confirms we need a trip state machine. The fee calculation changes based on which state the trip is in when cancelled.
You: "Does the system support a two-way rating system?"
Interviewer: "Yes. After trip completion, the rider rates the driver (1-5 stars) and the driver rates the rider. Both ratings feed into a running average. Drivers below a certain threshold get deactivated."
Two-way ratings mean both Rider and Driver maintain an average rating. The rating is a simple running average over recent trips, not a lifetime average.
You: "How does the system handle payment? Do we need to model full payment processing?"
Interviewer: "No. Assume a PaymentService interface that handles charging. Your design should calculate the fare and pass it to the payment service. Support cash and card as payment methods."
Good. Payment is an external dependency. We calculate the total fare and delegate charging to an interface.
You: "Should the system track driver location in real time?"
Interviewer: "Yes. Drivers report GPS coordinates every few seconds. The system uses the latest coordinates for matching and ETA calculation. Riders see the driver's live location during en-route and in-progress states."
Real-time location means drivers have a mutable location field. The spatial index must update as drivers move. Matching algorithms use the latest snapshot.
Perfect. You have clarified scope and ruled out unnecessary complexity. The core system handles three ride types, spatial driver matching with configurable strategies, a strict trip state machine, zone-based surge pricing, two-way ratings, and state-dependent cancellation rules.
Final Requirements
Functional Requirements:
- Riders request a trip by providing pickup location, drop-off location, and ride type (Economy, Premium, XL)
- The system finds nearby available drivers using spatial indexing within a configurable radius
- A matching strategy selects the best driver from the candidate pool (nearest, highest-rated, or best ETA)
- Trips follow a strict state machine: REQUESTED, MATCHING, DRIVER_ASSIGNED, EN_ROUTE, ARRIVED, IN_PROGRESS, COMPLETED, CANCELLED
- The fare calculator computes estimated fare using base fare + distance rate + time rate, multiplied by surge pricing
- Riders and drivers rate each other after trip completion; ratings feed a running average
- Cancellation rules depend on trip state, with time-based and state-based fee logic
Non-Functional Requirements:
- Thread safety for concurrent ride requests and driver assignment
- Extensibility for new ride types, matching strategies, and fare models
- Efficient spatial lookup for nearby drivers (no linear scan of all drivers)
Out of Scope:
- UI rendering and REST API layer
- Database persistence and ORM mapping
- Pool rides and ride splitting
- Scheduled or pre-booked rides
- Payment gateway integration (assume a
PaymentServiceinterface) - Driver onboarding and vehicle verification
- Real-time map rendering
Example Inputs and Outputs
Scenario 1: Successful Economy ride with surge pricing
- Rider requests Economy ride: pickup at (40.7128, -74.0060), drop-off at (40.7580, -73.9855)
- Zone surge multiplier: 1.5x
- System finds 4 nearby available Economy drivers within 5 km; nearest-first strategy picks Driver #42 (1.2 km away)
- Fare estimate: base $2.50 + (5.2 mi x $1.50/mi) + (18 min x $0.25/min) = $14.80, surged to $22.20
- Rider confirms. Trip moves through: REQUESTED, MATCHING, DRIVER_ASSIGNED, EN_ROUTE, ARRIVED, IN_PROGRESS, COMPLETED
- Rider rates driver 5 stars; driver rates rider 4 stars
Scenario 2: No drivers available
- Rider requests Premium ride: pickup at (40.7300, -74.0100)
- System searches 5 km radius: 0 Premium-eligible drivers found
- System expands to 10 km: still 0 found
- System returns: "No drivers available for Premium. Try Economy or wait."
Scenario 3: Late cancellation with fee
- Rider requests Economy ride, driver is assigned and goes en-route
- 5 minutes later, rider cancels (trip is in EN_ROUTE state)
- System charges cancellation fee: $5.00
- Driver is released back to available pool
Try It Yourself
Try it yourself
Before reading the solution, spend 20 minutes sketching your own class diagram. Focus on the trip state machine first, then think about how you would find nearby drivers without scanning every driver in the system. Consider what changes when you switch from "nearest driver" to "highest-rated driver." Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look at the nouns in your requirements: rider, driver, trip, location, vehicle, ride request, fare, rating, payment, cancellation. Not all of these deserve their own class, but they are your starting candidates.
A common mistake is cramming everything into a Trip god-class that handles matching, pricing, ratings, and state transitions. Good design means each class has a single, clear job.
| Entity | Responsibility | Key attributes |
|---|---|---|
| Rider | Represents a customer. Holds profile and rating. No ride logic. | name, phone, rating, paymentMethod |
| Driver | Represents a driver. Tracks availability, location, and rating. | name, vehicle, location, status, rating |
| Vehicle | Data holder for car details. Determines ride type eligibility. | make, model, licensePlate, capacity, vehicleType |
| Trip | The orchestrator. Manages the lifecycle of a single ride from request to completion. | rider, driver, pickup, dropOff, state, fareEstimate, actualFare |
| RideRequest | Value object capturing what the rider wants. Immutable after creation. | rider, pickup, dropOff, rideType, requestTime |
| Location | Value object for GPS coordinates. Used everywhere. | latitude, longitude |
| FareEstimate | Computed fare breakdown before rider confirms. Immutable. | baseFare, distanceCharge, timeCharge, surgeMultiplier, total |
| Rating | Value object for a single rating event. | rater, rated, stars, tripId |
| TripState | Enum representing the trip lifecycle stages. | REQUESTED, MATCHING, DRIVER_ASSIGNED, EN_ROUTE, ARRIVED, IN_PROGRESS, COMPLETED, CANCELLED |
Notice that Trip is the orchestrator while RideRequest is a simple value object. Merging them would violate SRP because a ride request is immutable input data, while a trip has a mutable lifecycle with state transitions. FareEstimate is separate from Trip because fare logic is complex enough to warrant its own calculation pipeline.
Step 2: Define Relationships and Class Design
Class Diagram
Deriving Trip's Interface
Trip is the central orchestrator. Every requirement ties back to it.
Deriving state from requirements:
| Requirement | What Trip must track |
|---|---|
| "Rider requests a trip" | The original ride request (immutable) |
| "System assigns driver" | The matched driver (nullable until assigned) |
| "Trip follows state machine" | Current state enum |
| "Fare calculated before confirmation" | The fare estimate |
| "Trip completes with actual fare" | Actual fare, start/end times |
This gives us the state:
Trip:
id: String
request: RideRequest
driver: Driver (nullable)
state: TripState
fareEstimate: FareEstimate
actualFare: double
startTime, endTime: LocalDateTime
Deriving methods from needs:
| Need | Method |
|---|---|
| "Assign driver to trip" | assignDriver(driver) |
| "Move trip through states" | transitionTo(newState) |
| "Complete with actual fare" | complete(actualFare) |
| "Cancel based on rules" | cancel() |
| "Check if cancellation allowed" | isCancellable() |
We make driver nullable. Before matching completes, there is no driver. The state machine enforces that assignDriver is only valid in the MATCHING state.
Deriving RideSharingService's Interface
This is the top-level facade that coordinates everything.
Deriving methods from requirements:
| Need | Method |
|---|---|
| "Rider requests a ride" | requestRide(request): Trip |
| "Cancel an active trip" | cancelTrip(trip) |
| "Complete a trip" | completeTrip(trip) |
| "Rate after completion" | rateDriver(trip, stars), rateRider(trip, stars) |
| "Update driver location" | updateDriverLocation(driverId, location) |
The service delegates spatial lookup to a SpatialIndex, driver selection to a DriverMatchingStrategy, and fare computation to a FareCalculator. It does not own any of that logic directly.
Key Relationship Decisions
Trip owns RideRequest by composition because a request does not exist independently of the trip it creates. Driver has a reference to Vehicle because drivers switch vehicles rarely, but we keep it as an association (not composition) since a vehicle can exist without a driver. RideSharingService depends on strategy interfaces, not concrete implementations, which is the core of the Strategy pattern.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.