Design a Ride-Sharing System

Open-ended practice prompt: matching, surge pricing, the three-sided state machine. Work it under 45 minutes.

Exercise Intermediate
11 min read
exercise ood ride-sharing state-machine

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 Ride can 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 — VehicleCategory is 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:

  • Ride HAS-A Rider, HAS-A Driver (nullable until matched), HAS-A Vehicle, HAS-A Location for pickup and drop-off, HAS-A Payment.
  • Driver HAS-A Vehicle. (A real driver might switch vehicles between shifts — model this as a current-vehicle reference, not as inheritance.)
  • DispatchService USES MatchingStrategy and PricingStrategy — both injected so they are swappable.
  • Ride is the aggregate root for the entire lifecycle. All state transitions go through methods on Ride, not through external code mutating fields.

Two things to avoid:

  • A Driver subclass per vehicle category. Use composition: Driver has a Vehicle, and Vehicle has a VehicleCategory.
  • Separate MatchedRide / InProgressRide classes. That is the State pattern done wrong — the state is on Ride as an enum (or a State object that lives on Ride), 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:

DrawSkip
The three actor classes and RideAddress parsing, geocoding
DispatchService with MatchingStrategy and PricingStrategy injectedA full Map / GeoIndex class
Payment as a value with status, but not the gatewayReceipt PDF formatting
The RideState, DriverState, RiderState enumsEvery 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.

  1. Rider.requestRide(pickup, dropoff, category) — Rider’s state goes Idle → Searching. The rider builds a RideRequest and hands it to DispatchService.
  2. DispatchService.requestRide(request) — quotes the fare via PricingStrategy.quote() (which consults SurgeMultiplier), then asks MatchingStrategy.findDriver() for a candidate.
  3. MatchingStrategy.findDriver() — returns the chosen Driver. The driver’s state is Available.
  4. DispatchService offers the ride to the driver. Driver.acceptRide(ride) — Driver’s state goes Available → Matched; Ride’s state goes Requested → Matched; Rider’s state goes Searching → Matched. Three transitions, one event.
  5. Driver picks up the rider. Ride.start() — Ride goes Matched → InProgress; Driver goes Matched → Driving; Rider goes Matched → Riding.
  6. Driver arrives. Ride.complete() — Ride goes InProgress → PaymentPending; Driver goes Driving → Available (after a brief Returning if you model the drop-off latency); Rider goes Riding → PaymentPending.
  7. Payment.charge() — succeeds; Ride goes PaymentPending → Completed; Rider goes PaymentPending → Idle.

Then narrate one unhappy path. Cancellation by the rider after match but before pickup:

  • Rider.cancelRide()Ride.cancel(by=RIDER).
  • Ride consults a CancellationPolicy — if Matched and time-since-match > threshold, charge a cancellation fee.
  • Ride goes Matched → Cancelled; Driver goes Matched → Available (and re-enters the pool); Rider goes Matched → 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.

DecisionRecommended answerThe 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 MatchingStrategy interface 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 SurgeMultiplier consulted by PricingStrategy. That lets us change surge policy without touching pricing, and lets the same surge object feed analytics. The alternative is a surgeMultiplier field on Ride, 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) from Ride.match(), then Ride has to know about the driver’s state machine. With Observer, Ride publishes RideMatched and the driver listens. Eventing also makes it easy to slot in analytics, push notifications, and the dispatch dashboard without changing Ride.”
  • In-memory RideRepository vs persistence. “In-memory satisfies the prompt. The RideRepository is an interface so we can plug in a database later — the seam is in the right place. For a real system I’d put Ride in a transactional store keyed by ride id, with Driver.location in a separate geospatial store.”
  • Cancellation policy as a method on Ride vs a policy class. “I put it in a CancellationPolicy class 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.

AxisStrong signalWeak signal
ScopingNames 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.
DecompositionRide 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 fluencyDraws 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 awarenessPicks 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.
CommunicationNarrates 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.

  • 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 MatchingStrategy and PricingStrategy.
  • Observer Pattern — the seam that keeps the three state machines in sync.
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.