Design a Ride-Sharing System
Open-ended practice prompt: matching, surge pricing, the three-sided state machine. Work it under 45 minutes.
Scenario#
You will be given a prompt of the form “design a ride-sharing system” — sometimes named after a concrete product (Uber, Lyft, Ola), sometimes generic. The interviewer wants to see whether you can model three actors whose states have to stay in sync: the rider, the driver, and the ride itself. The single hardest thing in this prompt is not the matching algorithm and not the pricing — it is that a single user action moves three state machines at once, and you have to draw a class diagram in which that synchronization has an obvious home.
What is actually being tested:
- Can you name the three state machines and their transitions without being prompted?
- Can you pick one matching strategy and one pricing strategy and defend them, rather than gesturing at a menu?
- Can you say what you are not building (routing, ETA, fraud, driver onboarding, regulatory compliance) early and stay disciplined about it?
- Can you handle a cancellation as a first-class flow, not a footnote?
This writeup is a structured walk-through of the 45-minute round for this specific prompt — entities, the three-sided state machine, the two strategies that show up, and the trade-offs the interviewer is most likely to push on.
Constraints#
The constraints are about the room, not the problem:
- 45 minutes. Roughly 5 for introductions, 30–35 working, 5 for the interviewer’s questions. The temptation in this prompt is to spend 20 minutes on matching; resist.
- Shared editor or whiteboard, with the interviewer reading along. The class diagram and the state-machine sketch are the artefacts they will remember.
- One language, usually Java. Code sketches stay tiny — the value is in the decomposition, not in compilable Java.
- In-process design. You are designing the domain model. If the interviewer asks “where does this live in a real deployment?” you can name a service boundary, but you do not draw infrastructure unless asked.
- Out of scope by default: turn-by-turn routing, ETA prediction, map tiles, driver background checks, payment gateway internals, accessibility features, regulatory geo-fencing, multi-rider pool variants. Name them out loud and move on.
The prompts vary; the structure does not.
Approach#
The 45-minute round breaks into the same five phases as any LLD round — only the entities differ. The minute targets below are the median; slip a little either way is fine.
Phase 1 — Clarify scope (5 minutes)#
The five questions worth asking, in roughly decreasing leverage:
- Who are the users? Rider and Driver are obvious. Is there an admin who suspends accounts? A dispatcher? A support agent who can cancel rides on behalf of a rider? Naming this fixes the use case diagram and the auth boundary.
- Single ride only, or do we model pooled / carpool? Pooled rides are a different beast — a
Ridecan contain multiple riders, and the state machine forks. Default to single-rider unless they push. - One vehicle category, or multiple (economy / premium / XL)? Multiple categories are usually expected and they are cheap to model —
VehicleCategoryis a separate enum or class that the matcher and pricer both consult. - Pricing model: flat per-distance, or surge-aware? Surge is the interesting case and is almost always wanted. Confirm.
- Cancellation policy: who can cancel and at what stages? Both sides can cancel. Charges depend on the state at cancel time. This shapes the state machine.
End by reading the scope back: “So we are designing single-rider matched rides for actors A and B with categories C1/C2/C3, surge-aware pricing, and bilateral cancellation — ignoring routing, ETA prediction, and onboarding. Right?”
Phase 2 — Identify entities and relationships (5 minutes)#
The nouns for this prompt cluster into three groups:
- Actors and accounts:
Rider,Driver,User(if you want a shared base),Vehicle. - The ride itself:
Ride,RideRequest,Location(a value object — latitude, longitude, optional address). - Policy and platform:
MatchingStrategy,PricingStrategy,SurgeMultiplier,Payment,RideRepository,DispatchService.
That is roughly 10–12 classes, which is the right size.
Relationships worth calling out:
RideHAS-ARider, HAS-ADriver(nullable until matched), HAS-AVehicle, HAS-ALocationfor pickup and drop-off, HAS-APayment.DriverHAS-AVehicle. (A real driver might switch vehicles between shifts — model this as a current-vehicle reference, not as inheritance.)DispatchServiceUSESMatchingStrategyandPricingStrategy— both injected so they are swappable.Rideis the aggregate root for the entire lifecycle. All state transitions go through methods onRide, not through external code mutating fields.
Two things to avoid:
- A
Driversubclass per vehicle category. Use composition:Driverhas aVehicle, andVehiclehas aVehicleCategory. - Separate
MatchedRide/InProgressRideclasses. That is the State pattern done wrong — the state is onRideas an enum (or a State object that lives onRide), not as a class hierarchy.
Phase 3 — Draw the class diagram (10–12 minutes)#
The core boxes and their important methods:
+----------------+ +----------------+ +----------------+| Rider | | Ride | | Driver |+----------------+ +----------------+ +----------------+| - id | | - id | | - id || - location | 1 * | - rider | * 1 | - location || - state |--------| - driver |--------| - vehicle |+----------------+ | - pickup | | - state || requestRide() | | - dropoff | +----------------+| cancelRide() | | - state | | acceptRide() |+----------------+ | - fare | | startTrip() | +----------------+ | endTrip() | | accept() | | goOffline() | | start() | +----------------+ | complete() | | cancel() | +----------------+ | | uses v +----------------+ +----------------+ | DispatchSvc |------->| MatchingStrat | +----------------+ +----------------+ | requestRide() | | findDriver() | | onAccept() | +----------------+ | onCancel() | +----------------+ +----------------+ |---------------->| PricingStrat | | +----------------+ | | quote() | | +----------------+Things to draw and to not draw:
| Draw | Skip |
|---|---|
The three actor classes and Ride | Address parsing, geocoding |
DispatchService with MatchingStrategy and PricingStrategy injected | A full Map / GeoIndex class |
Payment as a value with status, but not the gateway | Receipt PDF formatting |
The RideState, DriverState, RiderState enums | Every getter / setter |
Phase 4 — Narrate a flow (8–10 minutes)#
Walk through the happy path first as a sequence of object messages. This is the moment the three-sided state machine becomes visible — narrate the transitions out loud.
Rider.requestRide(pickup, dropoff, category)— Rider’s state goesIdle → Searching. The rider builds aRideRequestand hands it toDispatchService.DispatchService.requestRide(request)— quotes the fare viaPricingStrategy.quote()(which consultsSurgeMultiplier), then asksMatchingStrategy.findDriver()for a candidate.MatchingStrategy.findDriver()— returns the chosenDriver. The driver’s state isAvailable.DispatchServiceoffers the ride to the driver.Driver.acceptRide(ride)— Driver’s state goesAvailable → Matched; Ride’s state goesRequested → Matched; Rider’s state goesSearching → Matched. Three transitions, one event.- Driver picks up the rider.
Ride.start()— Ride goesMatched → InProgress; Driver goesMatched → Driving; Rider goesMatched → Riding. - Driver arrives.
Ride.complete()— Ride goesInProgress → PaymentPending; Driver goesDriving → Available(after a briefReturningif you model the drop-off latency); Rider goesRiding → PaymentPending. Payment.charge()— succeeds; Ride goesPaymentPending → Completed; Rider goesPaymentPending → Idle.
Then narrate one unhappy path. Cancellation by the rider after match but before pickup:
Rider.cancelRide()→Ride.cancel(by=RIDER).Rideconsults aCancellationPolicy— ifMatchedand time-since-match > threshold, charge a cancellation fee.- Ride goes
Matched → Cancelled; Driver goesMatched → Available(and re-enters the pool); Rider goesMatched → Idle.
The cancellation flow is where most candidates get caught — practising it once is worth more than rehearsing the happy path three times.
Phase 5 — Defend trade-offs (5–8 minutes)#
The trade-offs section below is the menu; the interviewer will pick two or three.
Design decisions to make#
Four decisions you must explicitly make on the board. Pre-rehearsing them is the highest-leverage prep for this prompt.
| Decision | Recommended answer | The trap |
|---|---|---|
| Where does the state machine live? | On Ride as the aggregate root. Rider and Driver have their own enums but transition only in response to events from Ride. | Three independent state machines with no orchestration — every cancellation has bugs. |
| Matching algorithm: nearest, weighted, or auction? | Strategy pattern with nearest-driver-with-rating-tiebreaker as the default. Surge and category constraints are inputs. | Hard-coding the matcher inside DispatchService; cannot swap for testing or for experiments. |
| Pricing: base or surge-aware? | Strategy pattern, with SurgeAwarePricing composing BasePricing and a SurgeMultiplier (a separate policy class). | Putting if (surge) branches inside Ride.complete(). |
| How do the three states stay in sync? | Observer pattern — Ride is the subject, Rider and Driver (or services that represent them) are observers. One transition fires N notifications. | Mutating rider.state and driver.state directly from Ride.start() etc. — couples three classes pairwise. |
If you can name your answer to each of these for this prompt, you have already done 60% of the design work.
Trade-offs to discuss#
The five most common follow-up trade-offs and the shape of a defensible answer.
- Strategy vs hard-coded matching. “I went with a
MatchingStrategyinterface because matching is the thing the business changes most often — nearest today, rating-weighted next quarter, surge-aware after that. The cost is one extra layer of indirection; the benefit is that A/B testing matchers is a constructor argument. If matching were a stable, settled algorithm, I’d inline it.” - Surge as a strategy decoration vs a price field. “Surge is a separate
SurgeMultiplierconsulted byPricingStrategy. That lets us change surge policy without touching pricing, and lets the same surge object feed analytics. The alternative is asurgeMultiplierfield onRide, but then we’d recompute surge in every place that needs it.” - Observer vs direct method calls between Ride and the actors. “Observer because the rider and driver are independently auditable — if I call
driver.transitionTo(Matched)fromRide.match(), thenRidehas to know about the driver’s state machine. With Observer,RidepublishesRideMatchedand the driver listens. Eventing also makes it easy to slot in analytics, push notifications, and the dispatch dashboard without changingRide.” - In-memory
RideRepositoryvs persistence. “In-memory satisfies the prompt. TheRideRepositoryis an interface so we can plug in a database later — the seam is in the right place. For a real system I’d putRidein a transactional store keyed by ride id, withDriver.locationin a separate geospatial store.” - Cancellation policy as a method on Ride vs a policy class. “I put it in a
CancellationPolicyclass because the rules differ by region and by category, and they change without warning.Ride.cancel()consults the policy to compute the fee and to decide whether re-dispatch is allowed.”
Evaluation criteria#
Interviewers usually rank candidates on five rough axes. Weak signals weigh more than strong ones.
| Axis | Strong signal | Weak signal |
|---|---|---|
| Scoping | Names the three actors and the three state machines in the first five minutes; pins down pooled-vs-single and cancellation policy. | Starts drawing the matcher before clarifying. |
| Decomposition | Ride as a clear aggregate root; matching and pricing as injected strategies; one or two well-chosen patterns (Strategy, Observer, State). | A Ride god class; matching inlined; one class per state. |
| State-machine fluency | Draws all three state machines and the synchronisation seam; handles cancellation as a first-class flow. | Only the happy path; cancellation as an afterthought; rider/driver states left implicit. |
| Trade-off awareness | Picks one matcher and one pricer and defends them; names the breakpoint at which the alternative wins. | Lists “we could use A or B” without committing. |
| Communication | Narrates as they draw; checks in after each phase; finishes the round with time for follow-ups. | Long silences during the state-machine sketch; abandons the original problem to dive into geospatial indexing. |
A passing round is consistent on scoping and state-machine fluency, with at least passable signal on the other three.
Related exercises#
- Approaching the OOD Interview — the meta-script. Read this first if you have not internalised the five phases.
- Design a Food Delivery System — the same three-sided shape (Customer / Restaurant / Agent) with different actors. Practising both makes the pattern stick.
- Parking Lot — the canonical worked system. Smaller state machine, but the same aggregate-root discipline.
- State Pattern — the formalism behind the three state machines in this exercise.
- Strategy Pattern — the formalism behind
MatchingStrategyandPricingStrategy. - Observer Pattern — the seam that keeps the three state machines in sync.