Low-Level Design Guide for Ride Booking Application
Abstract AlgorithmsTL;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...

TLDR: Designing Uber isn't just about maps; it's about managing state. A ride goes from
REQUESTEDtoACCEPTEDtoCOMPLETED. 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.
- The Call: A customer calls: "I'm at Central Station, going to the Airport."
- The Search: You look at your big map. You see 3 drivers nearby.
- The Assignment: You radio Driver A. "Are you free?" If yes, you assign the job.
- 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
Rideclass. Create aPricingStrategyinterface.
Design Pattern in Action: By using an interface, we can add a
PremiumPricingStrategylater 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
RideManageris 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
| Driver | Location (x, y) | Status | Rating |
| A | (0, 0) | Available | 4.8 |
| B | (2, 2) | Available | 4.5 |
| C | (10, 10) | Busy | 5.0 |
Rider Location: (1, 1)
Driver Assignment Logic
- Filter: Remove Driver C (Busy).
- 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.
- 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
| Strategy | Concept | Pros | Cons |
| Pessimistic Locking | Lock the driver immediately. No one else can check him. | Safe. Prevents conflicts. | Slow. Can cause bottlenecks. |
| Optimistic Locking | Let 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)
| Method | Endpoint | Description |
POST | /api/v1/rides | Create a new ride request. |
GET | /api/v1/rides/{id} | Get status of a ride. |
PUT | /api/v1/rides/{id}/status | Update status (e.g., Driver accepts). |
POST | /api/v1/drivers/location | Update 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?
Continuous Integration (CI):
- Unit Tests: Test
PricingStrategylogic (Surge math). - Integration Tests: Test
RideManagermatching logic with a mock database. - Tool: GitHub Actions runs these on every Pull Request.
- Unit Tests: Test
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.
- Docker: Containerize the application (
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
RideTypeand aCarpoolMatchingStrategy. The coreRideclass remains untouched.
Practice Quiz: Test Your Design Skills
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
PremiumPricingStrategyimplementing the interface. - C) Change the database schema.
- A) Add an
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.
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)

Written by
Abstract Algorithms
@abstractalgorithms
