Model a ride-sharing platform with driver matching, trip state machine, surge pricing strategy, and real-time status observer notifications.
Overview
A Ride-Sharing App matches Riders with nearby Drivers for Trips. Trip follows a strict status machine: REQUESTED → ACCEPTED → IN_PROGRESS → COMPLETED (or CANCELLED). PricingStrategy encapsulates normal versus surge pricing so the fare calculation can switch dynamically without touching Trip. MatchingService finds the nearest available Driver using location data. Observer notifies the Rider and a logging service on every status transition. Command pattern models trip actions (AcceptTripCommand, StartTripCommand) enabling auditability and potential replay. RideSharingApp is the Facade over Matching, Pricing, and Trip management.
Requirements Analysis
Functional: rider requests trip, nearest driver is matched, driver accepts/starts/completes trip, fare calculated on completion, rider and driver notified on every state change. Non-functional: pricing algorithm swappable at runtime (normal/surge), trip status transitions enforced by state machine, matching decoupled from pricing.
// Entities : RideSharingApp, Driver, Rider, Trip, TripStatus, Location, MatchingService
// Patterns : Strategy (pricing), Observer (status updates), State (trip lifecycle), Command (trip actions)Core Classes & Relationships
TripStatus enum: REQUESTED, ACCEPTED, IN_PROGRESS, COMPLETED, CANCELLED. Location is a record with lat and lon. PricingStrategy interface has calculateFare(double distanceKm, long durationSeconds). SurgePricing multiplies the base fare. TripStatusObserver interface fires on every transition. MatchingService.findNearestDriver() returns the closest available Driver.
public enum TripStatus { REQUESTED, ACCEPTED, IN_PROGRESS, COMPLETED, CANCELLED }
public record Location(double lat, double lon) {
public double distanceTo(Location other) {
double dlat = this.lat - other.lat;
double dlon = this.lon - other.lon;
return Math.sqrt(dlat * dlat + dlon * dlon) * 111.0; // rough km
}
}
public interface PricingStrategy {
double calculateFare(double distanceKm, long durationSeconds);
}
public class NormalPricing implements PricingStrategy {
@Override public double calculateFare(double km, long sec) {
return 30.0 + km * 12.0 + (sec / 60.0) * 1.5;
}
}
public class SurgePricing implements PricingStrategy {
private final double multiplier;
public SurgePricing(double multiplier) { this.multiplier = multiplier; }
@Override public double calculateFare(double km, long sec) {
return new NormalPricing().calculateFare(km, sec) * multiplier;
}
}
public interface TripStatusObserver {
void onStatusChange(String tripId, TripStatus oldStatus, TripStatus newStatus);
}Java Implementation
Driver tracks availability and current location. Trip enforces status transitions and notifies observers on each change. RideSharingApp.requestTrip() calls MatchingService, creates the Trip, and manages the lifecycle. MatchingService.findNearestDriver() filters available drivers and picks the minimum-distance one.
public class Driver {
private final String driverId;
private final String name;
private Location location;
private boolean available = true;
public Driver(String driverId, String name, Location location) {
this.driverId = driverId; this.name = name; this.location = location;
}
public String getDriverId() { return driverId; }
public Location getLocation() { return location; }
public boolean isAvailable() { return available; }
public void setAvailable(boolean a) { this.available = a; }
public void updateLocation(Location l) { this.location = l; }
}
public class Trip {
private final String tripId;
private final Rider rider;
private final Driver driver;
private final Location pickup;
private final Location dropoff;
private TripStatus status = TripStatus.REQUESTED;
private final long startEpoch = System.currentTimeMillis();
private final List<TripStatusObserver> observers = new ArrayList<>();
public Trip(String tripId, Rider rider, Driver driver, Location pickup, Location dropoff) {
this.tripId = tripId; this.rider = rider; this.driver = driver;
this.pickup = pickup; this.dropoff = dropoff;
}
public void addObserver(TripStatusObserver o) { observers.add(o); }
public void transition(TripStatus newStatus) {
TripStatus old = this.status;
this.status = newStatus;
observers.forEach(o -> o.onStatusChange(tripId, old, newStatus));
}
public double distanceKm() { return pickup.distanceTo(dropoff); }
public long durationSeconds() { return (System.currentTimeMillis() - startEpoch) / 1000; }
public TripStatus getStatus() { return status; }
public String getTripId() { return tripId; }
}
public class MatchingService {
public Optional<Driver> findNearestDriver(List<Driver> drivers, Location pickup) {
return drivers.stream()
.filter(Driver::isAvailable)
.min(Comparator.comparingDouble(d -> d.getLocation().distanceTo(pickup)));
}
}
public class RideSharingApp {
private final List<Driver> drivers = new ArrayList<>();
private final MatchingService matching = new MatchingService();
private PricingStrategy pricing;
private int tripCounter = 0;
public RideSharingApp(PricingStrategy pricing) { this.pricing = pricing; }
public void setPricing(PricingStrategy p) { this.pricing = p; }
public void registerDriver(Driver d) { drivers.add(d); }
public Trip requestTrip(Rider rider, Location pickup, Location dropoff) {
Driver driver = matching.findNearestDriver(drivers, pickup)
.orElseThrow(() -> new IllegalStateException("No drivers available"));
driver.setAvailable(false);
Trip trip = new Trip("TRP-" + (++tripCounter), rider, driver, pickup, dropoff);
trip.addObserver((id, from, to) ->
System.out.printf("Trip %s: %s -> %s%n", id, from, to));
return trip;
}
public double completeTrip(Trip trip) {
trip.transition(TripStatus.COMPLETED);
double fare = pricing.calculateFare(trip.distanceKm(), trip.durationSeconds());
System.out.printf("Trip %s fare: %.2f%n", trip.getTripId(), fare);
return fare;
}
}Key Points to Remember
- 1TripStatus transitions must be validated — COMPLETED cannot go back to IN_PROGRESS; enforce this with explicit transition guards.
- 2Strategy for pricing allows surge multiplier to be updated at runtime based on demand signals without restarting the app.
- 3MatchingService is isolated from Trip — changing matching algorithm (nearest vs best-rated) requires only a new MatchingService implementation.
- 4Observer on Trip decouples real-time notifications from the core ride lifecycle, enabling push, SMS, and audit logging simultaneously.
Interview Questions
Sign in to ask AriaHow would you implement driver location updates in real time and match based on ETA rather than distance?
How would you design the surge pricing trigger based on supply and demand metrics?
How would you handle a driver cancelling an accepted trip and reassigning to another driver?
Ask Aria about Design a Ride-Sharing App like Uber
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.