All Posts

Low-Level Design Guide for Ride Booking Application

Abstract AlgorithmsAbstract Algorithms
··7 min read

TL;DR

TLDR: Designing Uber isn't just about maps; it's about managing state. A ride goes from REQUESTED to ACCEPTED to COMPLETED. We use the Strategy Pattern for pricing (Surge vs. Standard) and the Observer Pattern to notify drivers. This guide walks thro...

Cover Image for Low-Level Design Guide for Ride Booking Application

TLDR: Designing Uber isn't just about maps; it's about managing state. A ride goes from REQUESTED to ACCEPTED to COMPLETED. We use the Strategy Pattern for pricing (Surge vs. Standard) and the Observer Pattern to notify drivers. This guide walks through the classes, relationships, and code.


1. The Problem Statement (The "No-Jargon" Explanation)

Imagine you are a Taxi Dispatcher in the 1990s.

  1. The Call: A customer calls: "I'm at Central Station, going to the Airport."
  2. The Search: You look at your big map. You see 3 drivers nearby.
  3. The Assignment: You radio Driver A. "Are you free?" If yes, you assign the job.
  4. The Ride: You track the time. When they arrive, you calculate the fare based on distance + traffic.

In software, we need to automate this dispatcher. We need to model:

  • Users: Riders and Drivers.
  • Ride: The core transaction.
  • Matching: The logic to find a driver.
  • Pricing: The logic to calculate the cost.

2. Core Classes & Relationships

We use Object-Oriented Design (OOD) to represent these entities.

Visualizing the Architecture

Imagine the relationships like this:

  • Rider creates a Ride.
  • RideManager finds a Driver.
  • PricingStrategy calculates the cost.
classDiagram
    class User {
        +int id
        +String name
        +double rating
    }
    class Rider {
        +List paymentMethods
    }
    class Driver {
        +Vehicle vehicle
        +boolean isAvailable
    }
    class Ride {
        +int id
        +Rider rider
        +Driver driver
        +RideStatus status
    }
    class RideManager {
        +createRide()
        +cancelRide()
    }

    User <|-- Rider
    User <|-- Driver
    RideManager --> Ride : Manages
    Ride --> Rider : Has
    Ride --> Driver : Has

The Actors

  • User (Base Class): id, name, rating.
  • Rider (Extends User): paymentMethods, rideHistory.
  • Driver (Extends User): vehicleDetails, currentLocation, availabilityStatus.

The Ride Lifecycle

The state of a ride flows linearly.

stateDiagram-v2
    [*] --> REQUESTED
    REQUESTED --> ACCEPTED : Driver Found
    ACCEPTED --> STARTED : Driver Arrives
    STARTED --> COMPLETED : Destination Reached
    REQUESTED --> CANCELLED : User Cancels
    COMPLETED --> [*]

The Managers

  • RideManager: Handles the lifecycle (create, update, cancel).
  • DriverManager: Manages driver availability and location updates.

3. Design Patterns in Action

We don't just write if-else statements. We use patterns to make the system flexible.

A. Strategy Pattern (For Pricing)

  • Problem: Pricing changes. Sometimes it's standard ($1/km), sometimes it's Surge ($2/km), sometimes it's a Flat Rate (Airport).
  • Solution: Don't hardcode logic in the Ride class. Create a PricingStrategy interface.

Design Pattern in Action: By using an interface, we can add a PremiumPricingStrategy later without touching the existing code. This follows the Open/Closed Principle.

interface PricingStrategy {
    double calculateFare(RideDetails details);
}

class DefaultPricingStrategy implements PricingStrategy {
    public double calculateFare(RideDetails details) {
        return details.distance * 1.0 + details.time * 0.5;
    }
}

class SurgePricingStrategy implements PricingStrategy {
    public double calculateFare(RideDetails details) {
        return (details.distance * 1.0 + details.time * 0.5) * 2.0; // 2x Multiplier
    }
}

B. Observer Pattern (For Driver Matching)

  • Problem: When a ride is requested, we need to notify all nearby drivers.
  • Solution: The RideManager is the Subject. Nearby Drivers are Observers.

Deep Dive: The Matching Algorithm (Finding a Driver)

How do we actually pick a driver? It's not random.

Toy Dataset: Nearby Drivers

DriverLocation (x, y)StatusRating
A(0, 0)Available4.8
B(2, 2)Available4.5
C(10, 10)Busy5.0

Rider Location: (1, 1)

Driver Assignment Logic

  1. Filter: Remove Driver C (Busy).
  2. Calculate Distance: Euclidean Distance $\sqrt{(x_2-x_1)^2 + (y_2-y_1)^2}$.
    • Driver A: $\sqrt{(1-0)^2 + (1-0)^2} = \sqrt{2} \approx 1.41$ km.
    • Driver B: $\sqrt{(2-1)^2 + (2-1)^2} = \sqrt{2} \approx 1.41$ km.
  3. Tie-Breaker: If distances are equal, pick the higher rating.
    • Winner: Driver A (4.8 > 4.5).

The Code Snippet:

public Driver findDriver(Location riderLoc, List<Driver> drivers) {
    Driver bestDriver = null;
    double minDistance = Double.MAX_VALUE;

    for (Driver d : drivers) {
        // 1. Filter unavailable drivers
        if (d.getStatus() != DriverStatus.AVAILABLE) continue;

        double dist = calculateDistance(riderLoc, d.getLocation());

        // 2. Find closest
        if (dist < minDistance) {
            minDistance = dist;
            bestDriver = d;
        }
    }
    return bestDriver;
}

4. Handling Concurrency (The "Race Condition")

Scenario: Two riders request a ride at the exact same second. There is only one driver nearby.

  • Problem: Both requests see the driver as "Available". Both try to book him.
  • Result: The driver gets two bookings. (Bad).

Concurrency Pitfalls: Optimistic vs. Pessimistic

StrategyConceptProsCons
Pessimistic LockingLock the driver immediately. No one else can check him.Safe. Prevents conflicts.Slow. Can cause bottlenecks.
Optimistic LockingLet everyone try. Check version at the end.Fast. High throughput.Requires retries if conflict happens.

Solution: Synchronized Locking (Pessimistic approach for simplicity) We need to lock the driver object when assigning a ride.

public boolean assignRide(Driver driver, Ride ride) {
    // Critical Section: Only one thread can enter here for this driver
    synchronized (driver) {
        if (driver.getStatus() == DriverStatus.AVAILABLE) {
            driver.setStatus(DriverStatus.BUSY);
            ride.setDriver(driver);
            return true; // Success
        } else {
            return false; // Driver was taken by someone else 1ms ago
        }
    }
}

5. System Design: API & Database

To make this a real application, we need to expose APIs and store data.

API Design (REST)

MethodEndpointDescription
POST/api/v1/ridesCreate a new ride request.
GET/api/v1/rides/{id}Get status of a ride.
PUT/api/v1/rides/{id}/statusUpdate status (e.g., Driver accepts).
POST/api/v1/drivers/locationUpdate driver's GPS coordinates.

Database Schema (SQL)

We use a Relational Database (PostgreSQL/MySQL) because ride transactions require ACID properties.

Table: Users

  • id (PK), name, email, type (RIDER/DRIVER)

Table: Drivers

  • user_id (FK), vehicle_plate, current_lat, current_long, status (AVAILABLE/BUSY)

Table: Rides

  • id (PK), rider_id (FK), driver_id (FK), source_lat, source_long, dest_lat, dest_long, status, fare, created_at

6. CI/CD & Deployment Strategy

How do we ship this code safely?

  1. Continuous Integration (CI):

    • Unit Tests: Test PricingStrategy logic (Surge math).
    • Integration Tests: Test RideManager matching logic with a mock database.
    • Tool: GitHub Actions runs these on every Pull Request.
  2. Continuous Deployment (CD):

    • Docker: Containerize the application (Dockerfile).
    • Blue-Green Deployment: Deploy the new version (Green) alongside the old one (Blue). Switch traffic only if Green is healthy. This prevents downtime if a bug slips through.

Summary & Key Takeaways

  • Actors: Separate Rider and Driver, but inherit from a common User.
  • Strategy Pattern: Use this for Pricing (Surge/Standard) and Matching (Nearest/Highest Rated).
  • State Management: A Ride has a lifecycle (Created -> Accepted -> Started -> Completed).
  • Concurrency: Always lock the resource (Driver) before assigning to prevent double-booking.

Future Proofing: This design is modular. If we want to add Carpooling, we just add a new RideType and a CarpoolMatchingStrategy. The core Ride class remains untouched.


Practice Quiz: Test Your Design Skills

  1. Scenario: You want to add a new "Premium Car" pricing model where the base fare is $5 higher. Using the Strategy Pattern, what do you do?

    • A) Add an if (type == PREMIUM) check in the main class.
    • B) Create a new class PremiumPricingStrategy implementing the interface.
    • C) Change the database schema.
  2. Scenario: A driver accepts a ride, but his internet disconnects before the server receives the confirmation. Meanwhile, the server assigns the ride to another driver. When the first driver reconnects, he thinks he has the ride. How do you fix this?

    • A) Optimistic Locking (Version Check).
    • B) Send an email to support.
    • C) Allow two drivers for one ride.
  3. Scenario: Which Design Pattern is best for notifying the Rider when the Driver has arrived?

    • A) Singleton Pattern
    • B) Observer Pattern
    • C) Factory Pattern

(Answers: 1-B, 2-A, 3-B)

Abstract Algorithms

Written by

Abstract Algorithms

@abstractalgorithms