Loading learning content...
With requirements documented, we now identify and model the core entities that form the backbone of our ride-sharing system. Entity identification is where abstract requirements become concrete classes with attributes and behaviors.
The noun extraction technique:
Read through the requirements and use cases, extracting every significant noun. These nouns are entity candidates. Then filter: Is this an independent concept with its own identity and lifecycle? Or is it an attribute of another entity, or merely a value with no identity?
From our requirements, the primary nouns that emerge are: Rider, Driver, Trip (or Ride), Location, Vehicle, Fare, Rating, RideRequest, and RideOffer. Let's systematically model each.
By the end of this page, you will understand how to model the core entities of a ride-sharing system. You'll design Rider and Driver with their distinct responsibilities, the Trip entity as the central aggregate, Location as a value object, and understand the relationships binding them together.
Before modeling actors and trips, we must define Location—the geographic primitive that permeates every aspect of ride-sharing. Location appears in pickup points, destinations, driver positions, and route waypoints.
Value Object vs Entity:
Location is a value object, not an entity. It has no unique identity—two locations at the same coordinates are interchangeable. It's immutable (coordinates don't change; a new location is created), and equality is based on attribute values, not identity.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
/** * Location represents a geographic point. * Immutable value object - equality based on coordinates. */class Location { private readonly latitude: number; private readonly longitude: number; private readonly address?: string; // Human-readable address private readonly placeId?: string; // External geocoding reference constructor(latitude: number, longitude: number, address?: string, placeId?: string) { this.validateCoordinates(latitude, longitude); this.latitude = latitude; this.longitude = longitude; this.address = address; this.placeId = placeId; } private validateCoordinates(lat: number, lng: number): void { if (lat < -90 || lat > 90) throw new Error("Invalid latitude"); if (lng < -180 || lng > 180) throw new Error("Invalid longitude"); } getLatitude(): number { return this.latitude; } getLongitude(): number { return this.longitude; } getAddress(): string | undefined { return this.address; } /** * Calculate distance to another location using Haversine formula. * Returns distance in kilometers. */ distanceTo(other: Location): number { const R = 6371; // Earth's radius in km const dLat = this.toRadians(other.latitude - this.latitude); const dLon = this.toRadians(other.longitude - this.longitude); const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(this.toRadians(this.latitude)) * Math.cos(this.toRadians(other.latitude)) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; } private toRadians(degrees: number): number { return degrees * (Math.PI / 180); } equals(other: Location): boolean { return this.latitude === other.latitude && this.longitude === other.longitude; } toString(): string { return this.address || `(${this.latitude}, ${this.longitude})`; }}The Haversine formula calculates great-circle distance between two points on a sphere. For ride-sharing distances (typically < 50km), it's sufficiently accurate. Production systems might use road-network distance via external APIs, but Haversine is the standard for 'as the crow flies' proximity calculations in matching.
The Rider entity represents users who request transportation. Unlike Location (value object), Rider has unique identity, mutable state, and a lifecycle spanning account creation through many trips.
Rider responsibilities:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
enum RiderStatus { IDLE = 'IDLE', // No active trip REQUESTING = 'REQUESTING', // Ride request pending WAITING = 'WAITING', // Matched, waiting for driver IN_TRIP = 'IN_TRIP', // Currently on a trip} interface PaymentMethod { id: string; type: 'CARD' | 'WALLET' | 'CASH'; isDefault: boolean; lastFour?: string; // For cards} class Rider { private readonly id: string; private name: string; private email: string; private phone: string; private status: RiderStatus; private rating: number; private totalTrips: number; private paymentMethods: PaymentMethod[]; private defaultPaymentId: string | null; private savedLocations: Map<string, Location>; // "home", "work", etc. private currentLocation: Location | null; private createdAt: Date; constructor(id: string, name: string, email: string, phone: string) { this.id = id; this.name = name; this.email = email; this.phone = phone; this.status = RiderStatus.IDLE; this.rating = 5.0; // Start with perfect rating this.totalTrips = 0; this.paymentMethods = []; this.defaultPaymentId = null; this.savedLocations = new Map(); this.currentLocation = null; this.createdAt = new Date(); } // Identity getId(): string { return this.id; } getName(): string { return this.name; } // Status management getStatus(): RiderStatus { return this.status; } isAvailableForTrip(): boolean { return this.status === RiderStatus.IDLE; } setStatus(status: RiderStatus): void { this.status = status; } // Rating getRating(): number { return this.rating; } updateRating(newRating: number): void { // Rolling average const totalRatingPoints = this.rating * this.totalTrips + newRating; this.totalTrips++; this.rating = totalRatingPoints / this.totalTrips; } // Payment addPaymentMethod(method: PaymentMethod): void { this.paymentMethods.push(method); if (method.isDefault || this.paymentMethods.length === 1) { this.defaultPaymentId = method.id; } } getDefaultPaymentMethod(): PaymentMethod | null { return this.paymentMethods.find(m => m.id === this.defaultPaymentId) || null; } hasValidPayment(): boolean { return this.paymentMethods.length > 0; } // Location updateLocation(location: Location): void { this.currentLocation = location; } getCurrentLocation(): Location | null { return this.currentLocation; } saveFavoriteLocation(label: string, location: Location): void { this.savedLocations.set(label.toLowerCase(), location); } getSavedLocation(label: string): Location | undefined { return this.savedLocations.get(label.toLowerCase()); }}| Attribute | Type | Purpose |
|---|---|---|
| id | string (UUID) | Unique identifier |
| name, email, phone | string | Contact and identity |
| status | RiderStatus enum | Current state in system |
| rating | number (1.0-5.0) | Average rating from drivers |
| paymentMethods | PaymentMethod[] | Stored payment options |
| savedLocations | Map<string, Location> | Home, work, favorites |
| currentLocation | Location | null | Last known GPS position |
The Driver entity represents service providers who fulfill ride requests. Drivers have more complex state than riders—they can be offline, online (available), or engaged in a trip. They also have associated vehicles and earnings.
Driver responsibilities:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
enum DriverStatus { OFFLINE = 'OFFLINE', // Not accepting rides AVAILABLE = 'AVAILABLE', // Online, ready for rides ASSIGNED = 'ASSIGNED', // Has accepted a ride, heading to pickup IN_TRIP = 'IN_TRIP', // Currently executing a trip} interface Vehicle { id: string; make: string; model: string; year: number; color: string; licensePlate: string; type: VehicleType; capacity: number;} enum VehicleType { STANDARD = 'STANDARD', PREMIUM = 'PREMIUM', XL = 'XL', BIKE = 'BIKE',} class Driver { private readonly id: string; private name: string; private email: string; private phone: string; private profilePhotoUrl: string; private status: DriverStatus; private currentLocation: Location | null; private vehicle: Vehicle; private rating: number; private totalTrips: number; private acceptanceRate: number; private cancellationRate: number; private earnings: Map<string, number>; // date -> earnings private lastLocationUpdate: Date | null; private createdAt: Date; constructor( id: string, name: string, email: string, phone: string, vehicle: Vehicle ) { this.id = id; this.name = name; this.email = email; this.phone = phone; this.profilePhotoUrl = ''; this.status = DriverStatus.OFFLINE; this.currentLocation = null; this.vehicle = vehicle; this.rating = 5.0; this.totalTrips = 0; this.acceptanceRate = 1.0; this.cancellationRate = 0.0; this.earnings = new Map(); this.lastLocationUpdate = null; this.createdAt = new Date(); } // Identity getId(): string { return this.id; } getName(): string { return this.name; } getProfilePhotoUrl(): string { return this.profilePhotoUrl; } // Vehicle getVehicle(): Vehicle { return this.vehicle; } getVehicleType(): VehicleType { return this.vehicle.type; } // Status management getStatus(): DriverStatus { return this.status; } goOnline(): void { if (this.status === DriverStatus.OFFLINE) { this.status = DriverStatus.AVAILABLE; } } goOffline(): void { if (this.status === DriverStatus.AVAILABLE) { this.status = DriverStatus.OFFLINE; this.currentLocation = null; } } isAvailableForRide(): boolean { return this.status === DriverStatus.AVAILABLE; } setStatus(status: DriverStatus): void { this.status = status; } // Location (continuously updated when online) updateLocation(location: Location): void { this.currentLocation = location; this.lastLocationUpdate = new Date(); } getCurrentLocation(): Location | null { return this.currentLocation; } getLastLocationUpdate(): Date | null { return this.lastLocationUpdate; } isLocationFresh(maxAgeSeconds: number = 30): boolean { if (!this.lastLocationUpdate) return false; const ageMs = Date.now() - this.lastLocationUpdate.getTime(); return ageMs < maxAgeSeconds * 1000; } // Rating and performance getRating(): number { return this.rating; } getAcceptanceRate(): number { return this.acceptanceRate; } getCancellationRate(): number { return this.cancellationRate; } updateRating(newRating: number): void { const totalPoints = this.rating * this.totalTrips + newRating; this.totalTrips++; this.rating = totalPoints / this.totalTrips; } recordAcceptance(accepted: boolean): void { // Update rolling acceptance rate // Implementation would track last N offers } // Earnings addEarning(amount: number, date: Date = new Date()): void { const dateKey = date.toISOString().split('T')[0]; const current = this.earnings.get(dateKey) || 0; this.earnings.set(dateKey, current + amount); } getTodayEarnings(): number { const today = new Date().toISOString().split('T')[0]; return this.earnings.get(today) || 0; }}We model Vehicle as a separate interface (or class) composed into Driver. This supports scenarios like drivers using different vehicles on different days, or multiple drivers sharing a vehicle. In interviews, you can simplify to embedded vehicle properties, but noting the separation shows design foresight.
The Trip entity is the heart of the ride-sharing system—the aggregate that binds rider, driver, locations, timing, pricing, and state into a cohesive unit. Trip has the most complex lifecycle, governed by the state machine we outlined earlier.
Trip as an aggregate root:
In Domain-Driven Design terms, Trip is an aggregate root. All access to trip-related data (pickup location, fare, driver assignment) goes through the Trip entity. This ensures consistency—you can't modify a Trip's driver without going through Trip's methods, which enforce invariants.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
enum TripState { REQUESTED = 'REQUESTED', MATCHING = 'MATCHING', DRIVER_ASSIGNED = 'DRIVER_ASSIGNED', DRIVER_ARRIVED = 'DRIVER_ARRIVED', TRIP_IN_PROGRESS = 'TRIP_IN_PROGRESS', TRIP_COMPLETED = 'TRIP_COMPLETED', CANCELLED_BY_RIDER = 'CANCELLED_BY_RIDER', CANCELLED_BY_DRIVER = 'CANCELLED_BY_DRIVER', CANCELLED_NO_SHOW = 'CANCELLED_NO_SHOW', NO_DRIVERS_AVAILABLE = 'NO_DRIVERS_AVAILABLE',} interface Fare { baseFare: number; distanceFare: number; timeFare: number; surgeMultiplier: number; totalFare: number; currency: string;} class Trip { private readonly id: string; private rider: Rider; private driver: Driver | null; private pickupLocation: Location; private destination: Location; private state: TripState; private estimatedFare: Fare; private actualFare: Fare | null; private requestedAt: Date; private matchedAt: Date | null; private driverArrivedAt: Date | null; private tripStartedAt: Date | null; private tripCompletedAt: Date | null; private cancelledAt: Date | null; private cancellationReason: string | null; private actualRoute: Location[]; // Waypoints during trip private riderRating: number | null; private driverRating: number | null; constructor( id: string, rider: Rider, pickupLocation: Location, destination: Location, estimatedFare: Fare ) { this.id = id; this.rider = rider; this.driver = null; this.pickupLocation = pickupLocation; this.destination = destination; this.state = TripState.REQUESTED; this.estimatedFare = estimatedFare; this.actualFare = null; this.requestedAt = new Date(); this.matchedAt = null; this.driverArrivedAt = null; this.tripStartedAt = null; this.tripCompletedAt = null; this.cancelledAt = null; this.cancellationReason = null; this.actualRoute = []; this.riderRating = null; this.driverRating = null; } // Identity and accessors getId(): string { return this.id; } getRider(): Rider { return this.rider; } getDriver(): Driver | null { return this.driver; } getPickupLocation(): Location { return this.pickupLocation; } getDestination(): Location { return this.destination; } getState(): TripState { return this.state; } getEstimatedFare(): Fare { return this.estimatedFare; } getActualFare(): Fare | null { return this.actualFare; } // State transitions with validation startMatching(): void { this.validateTransition(TripState.MATCHING); this.state = TripState.MATCHING; } assignDriver(driver: Driver): void { this.validateTransition(TripState.DRIVER_ASSIGNED); this.driver = driver; this.state = TripState.DRIVER_ASSIGNED; this.matchedAt = new Date(); } driverArrived(): void { this.validateTransition(TripState.DRIVER_ARRIVED); this.state = TripState.DRIVER_ARRIVED; this.driverArrivedAt = new Date(); } startTrip(): void { this.validateTransition(TripState.TRIP_IN_PROGRESS); this.state = TripState.TRIP_IN_PROGRESS; this.tripStartedAt = new Date(); } completeTrip(actualFare: Fare): void { this.validateTransition(TripState.TRIP_COMPLETED); this.state = TripState.TRIP_COMPLETED; this.actualFare = actualFare; this.tripCompletedAt = new Date(); } cancelByRider(reason?: string): void { this.validateCancellable(); this.state = TripState.CANCELLED_BY_RIDER; this.cancelledAt = new Date(); this.cancellationReason = reason || 'Rider cancelled'; } cancelByDriver(reason?: string): void { this.validateCancellable(); this.state = TripState.CANCELLED_BY_DRIVER; this.cancelledAt = new Date(); this.cancellationReason = reason || 'Driver cancelled'; } private validateTransition(targetState: TripState): void { const validTransitions: Map<TripState, TripState[]> = new Map([ [TripState.REQUESTED, [TripState.MATCHING]], [TripState.MATCHING, [TripState.DRIVER_ASSIGNED, TripState.NO_DRIVERS_AVAILABLE]], [TripState.DRIVER_ASSIGNED, [TripState.DRIVER_ARRIVED]], [TripState.DRIVER_ARRIVED, [TripState.TRIP_IN_PROGRESS]], [TripState.TRIP_IN_PROGRESS, [TripState.TRIP_COMPLETED]], ]); const allowed = validTransitions.get(this.state) || []; if (!allowed.includes(targetState)) { throw new Error(`Invalid transition from ${this.state} to ${targetState}`); } } private validateCancellable(): void { const cancellableStates = [ TripState.REQUESTED, TripState.MATCHING, TripState.DRIVER_ASSIGNED, TripState.DRIVER_ARRIVED ]; if (!cancellableStates.includes(this.state)) { throw new Error(`Cannot cancel trip in state ${this.state}`); } } // Duration calculations getTripDuration(): number | null { if (!this.tripStartedAt || !this.tripCompletedAt) return null; return this.tripCompletedAt.getTime() - this.tripStartedAt.getTime(); } getWaitTime(): number | null { if (!this.matchedAt || !this.driverArrivedAt) return null; return this.driverArrivedAt.getTime() - this.matchedAt.getTime(); } // Route tracking recordWaypoint(location: Location): void { if (this.state === TripState.TRIP_IN_PROGRESS) { this.actualRoute.push(location); } } getActualDistance(): number { if (this.actualRoute.length < 2) return 0; let total = 0; for (let i = 1; i < this.actualRoute.length; i++) { total += this.actualRoute[i-1].distanceTo(this.actualRoute[i]); } return total; } // Ratings setRiderRating(rating: number): void { if (this.state !== TripState.TRIP_COMPLETED) { throw new Error('Can only rate after trip completion'); } this.riderRating = rating; } setDriverRating(rating: number): void { if (this.state !== TripState.TRIP_COMPLETED) { throw new Error('Can only rate after trip completion'); } this.driverRating = rating; } isComplete(): boolean { return this.state === TripState.TRIP_COMPLETED; } isCancelled(): boolean { return [ TripState.CANCELLED_BY_RIDER, TripState.CANCELLED_BY_DRIVER, TripState.CANCELLED_NO_SHOW, TripState.NO_DRIVERS_AVAILABLE ].includes(this.state); }}Notice how validateTransition() prevents illegal state changes. You cannot go from REQUESTED directly to TRIP_IN_PROGRESS—you must follow the defined path. This enforcement at the entity level prevents bugs that would otherwise require extensive testing to catch.
Understanding how entities relate is crucial for proper modeling. Let's analyze the key relationships in our ride-sharing domain.
| Relationship | Type | Cardinality | Notes |
|---|---|---|---|
| Rider → Trip | Association | 1 to Many | Rider has trip history; one active trip at a time |
| Driver → Trip | Association | 1 to Many | Driver completes many trips; one active at a time |
| Trip → Rider | Composition | Many to 1 | Trip requires a rider; rider outlives trip |
| Trip → Driver | Association | Many to 1 | Trip assigned to driver; can be null pre-match |
| Driver → Vehicle | Composition/Aggregation | 1 to 1* | Driver has a vehicle; could change vehicles |
| Trip → Location (pickup) | Composition | 1 to 1 | Pickup is part of trip; immutable |
| Trip → Location (destination) | Composition | 1 to 1 | Destination is part of trip; may change mid-trip |
| Trip → Fare | Composition | 1 to 1 | Fare calculated for trip; trip owns it |
// Conceptual Class Relationships (UML-style notation) ┌─────────────┐ ┌─────────────┐│ Rider │ │ Driver │├─────────────┤ ├─────────────┤│ -id: string │ │ -id: string ││ -name │ │ -name ││ -status │ │ -status ││ -rating │ │ -vehicle │─────────┐│ -location │ │ -location │ │└──────┬──────┘ └──────┬──────┘ │ │ 1 │ 0..1 │ │ │ │ │ requests │ executes │ │ │ │ ▼ * ▼ * │┌─────────────────────────────────────┐ ││ Trip │ │├─────────────────────────────────────┤ ││ -id: string │ ││ -state: TripState │ ││ -pickupLocation: Location ◆─────────┼───────┼──┐│ -destination: Location ◆────────────┼───────┼──┤│ -estimatedFare: Fare ◆──────────────┤ │ ││ -actualFare: Fare ◆─────────────────┤ │ ││ -timestamps │ │ │└─────────────────────────────────────┘ │ │ ◆ = Composition (Trip owns) │ │ ─ = Association │ │ │ │┌─────────────┐ ┌─────────────┐ │ ││ Vehicle │◄──────┤ │ │ │├─────────────┤ │ Location │◄────────┴──┘│ -make │ ├─────────────┤│ -model │ │ -latitude │ (Value Object)│ -type │ │ -longitude ││ -plate │ │ -address │└─────────────┘ └─────────────┘What's next:
With core entities defined, we'll design the matching algorithm—how the system finds the best driver for a ride request. This involves geospatial queries, ranking strategies, and handling the offer-accept-decline flow.
You now have a comprehensive entity model for ride-sharing: Location (value object), Rider (consumer entity), Driver (provider entity with vehicle), and Trip (central aggregate with state machine). These building blocks will be orchestrated by matching algorithms and pricing strategies in upcoming pages.