Car Rental System LLD – Complete Low-Level Design with UML & Classes
A Low-Level Design (LLD) architecture for an enterprise-grade Car Rental System. The system handles multi-location inventory management, dynamic vehicle categorization, flexible reservation lifecycles, configurable pricing models, and pluggable payment integration.
1. Problem Statement: Car Rental vs. Car Booking
A critical distinction must be made between a Car Booking System (e.g., ride-hailing services like Uber or Ola) and a Car Rental System (e.g., asset-leasing platforms like Hertz or Zoomcar):
- Car Booking System (Ride-Hailing): Matches dynamic demand with independent drivers in real time. The platform does not own the assets, and bookings represent point-to-point dynamic driver dispatching.
- Car Rental System (Asset Inventory Leasing): Manages a physical inventory of company-owned assets (vehicles) stored across distributed geographical stores. Users search available inventory at specific pickup/drop stores, reserve vehicles for defined time windows, collect the asset, and return it to a designated store.
Core Functional Requirements
- Multi-Store Fleet Inventory: Manage physical rental stores across multiple geographical locations, each housing an inventory of diverse vehicles (Cars, Bikes, Scooties, SUVs, Luxury Vehicles).
- Vehicle Search & Availability Filtering: Allow users to query available vehicles by store location, vehicle type, and start/end time windows.
- Reservation Management: Support complete reservation lifecycles—creation, confirmation, rental start, rental completion, and cancellation.
- Pluggable Payment Processing: Support multiple payment methods (Credit Card, Debit Card, Cash, PayPal) executing via dynamic payment strategies.
- Extensible Pricing Models: Support distinct pricing calculation models (Hourly, Daily, Weekly) decoupled from base vehicle properties.
2. Interview Strategy & Clarifying Questions
In a 45-minute system design interview, simulating a real-world architectural discussion requires structured enactment of customer and manager workflows.
Process Enactment Framework
To derive requirements naturally:
- Inventory Admin Persona: Onboards stores, adds physical vehicles, updates vehicle operational statuses, and tracks store fleet density.
- Customer Persona: Filters vehicles by location and temporal availability, chooses preferences (vehicle category, color, pricing tier), creates a reservation, completes payment, picks up the asset, and returns it.
Strategic Clarifying Questions & Pivot Points
- Candidate: "Are we strictly designing for four-wheeler cars, or should the design accommodate broader vehicle classifications such as motorbikes and scooters?"
- Interviewer Focus: Support arbitrary vehicle types dynamically while retaining type safety.
- Candidate: "Should pickup and drop-off locations be locked to the same store, or should we support cross-store drop-offs?"
- Interviewer Focus: Support distinct pickup and drop-off store locations in reservations.
- Candidate: "Are pricing models static base rates, or do they require dynamic rate evaluation based on temporal duration (hourly vs. daily vs. weekly)?"
- Interviewer Focus: Decouple pricing logic via extensible design patterns rather than hardcoding.
- Candidate: "How should concurrent asset reservations for overlapping date ranges be handled?"
- Interviewer Focus: Focus on entity modeling, object-oriented principles, modularity, and design patterns first. Handle time-range boundary validation within inventory stores (concurrency locks can be layered via pessimistic/optimistic database locks).
3. Core Entities & Conceptual Framework
The entity boundary identification follows the Single Responsibility Principle ():
Vehicle: Abstract representation of an inventory item possessing registration details, make, model, operational status, and base price.RentalStore: Physical operational store located at a specific geographical address, holding physical vehicle instances.User: System customer carrying identification metrics and historical reservation bindings.Reservation: Aggregate entity binding aUser, aVehicle, pickup/dropRentalStorelocations, start/end timestamps, pricing strategy, and lifecycle status.ReservationManager: Domain controller managing CRUD operations, lookup queries, and lifecycle state changes for reservations.RentalSystem: Central Singleton façade delegating operational calls to subsystem managers.
4. Applied Design Patterns & Architectural Blueprint
5. Class Diagram & System Architecture Flow
System Call Lifecycle Flow
6. Core Domain Models & Concrete Implementations
Enums for Strong Type Safety
package com.carrental.model;
public enum VehicleType {
ECONOMY,
LUXURY,
SUV,
BIKE,
SCOOTY
}
public enum VehicleStatus {
AVAILABLE,
RESERVED,
UNDER_MAINTENANCE,
DECOMMISSIONED
}
public enum ReservationStatus {
PENDING,
CONFIRMED,
IN_PROGRESS,
COMPLETED,
CANCELLED
}
Abstract Base Vehicle and Concrete Subclasses
package com.carrental.model;
public abstract class Vehicle {
private String registrationNumber;
private String make;
private String model;
private int year;
private double baseRentalPrice;
private VehicleStatus status;
private VehicleType vehicleType;
public Vehicle(String registrationNumber, String make, String model, int year, double baseRentalPrice, VehicleType vehicleType) {
this.registrationNumber = registrationNumber;
this.make = make;
this.model = model;
this.year = year;
this.baseRentalPrice = baseRentalPrice;
this.vehicleType = vehicleType;
this.status = VehicleStatus.AVAILABLE;
}
public abstract double calculateRentalFee(long durationUnits);
// Getters and Setters
public String getRegistrationNumber() { return registrationNumber; }
public String getMake() { return make; }
public String getModel() { return model; }
public int getYear() { return year; }
public double getBaseRentalPrice() { return baseRentalPrice; }
public VehicleStatus getStatus() { return status; }
public void setStatus(VehicleStatus status) { this.status = status; }
public VehicleType getVehicleType() { return vehicleType; }
}
package com.carrental.model;
public class EconomyVehicle extends Vehicle {
public EconomyVehicle(String registrationNumber, String make, String model, int year, double baseRentalPrice) {
super(registrationNumber, make, model, year, baseRentalPrice, VehicleType.ECONOMY);
}
@Override
public double calculateRentalFee(long durationUnits) {
// Base flat rate calculation for Economy vehicles
return getBaseRentalPrice() * durationUnits;
}
}
public class LuxuryVehicle extends Vehicle {
private static final double LUXURY_SURCHARGE_MULTIPLIER = 1.5;
public LuxuryVehicle(String registrationNumber, String make, String model, int year, double baseRentalPrice) {
super(registrationNumber, make, model, year, baseRentalPrice, VehicleType.LUXURY);
}
@Override
public double calculateRentalFee(long durationUnits) {
return getBaseRentalPrice() * durationUnits * LUXURY_SURCHARGE_MULTIPLIER;
}
}
public class SUVVehicle extends Vehicle {
private static final double SUV_FLAT_FEE = 20.0;
public SUVVehicle(String registrationNumber, String make, String model, int year, double baseRentalPrice) {
super(registrationNumber, make, model, year, baseRentalPrice, VehicleType.SUV);
}
@Override
public double calculateRentalFee(long durationUnits) {
return (getBaseRentalPrice() * durationUnits) + SUV_FLAT_FEE;
}
}
7. Vehicle Creation via Factory Pattern
The VehicleFactory isolates object instantiation logic from the caller, enforcing uniform vehicle construction rules across the system.
package com.carrental.factory;
import com.carrental.model.*;
public class VehicleFactory {
public static Vehicle createVehicle(VehicleType type, String regNumber, String make, String model, int year, double basePrice) {
if (type == null) {
throw new IllegalArgumentException("VehicleType cannot be null");
}
switch (type) {
case ECONOMY:
return new EconomyVehicle(regNumber, make, model, year, basePrice);
case LUXURY:
return new LuxuryVehicle(regNumber, make, model, year, basePrice);
case SUV:
return new SUVVehicle(regNumber, make, model, year, basePrice);
default:
throw new IllegalArgumentException("Unsupported vehicle type: " + type);
}
}
}
8. Inventory & Rental Store Management
Location Value Object
package com.carrental.model;
public class Location {
private String address;
private String city;
private String state;
private String zipCode;
public Location(String address, String city, String state, String zipCode) {
this.address = address;
this.city = city;
this.state = state;
this.zipCode = zipCode;
}
public String getCity() { return city; }
public String getAddress() { return address; }
@Override
public String toString() {
return address + ", " + city + ", " + state + " " + zipCode;
}
}
Rental Store Aggregate
Each RentalStore retains an isolated inventory of vehicles and provides methods to query vehicle availability for targeted date ranges.
package com.carrental.store;
import com.carrental.model.Location;
import com.carrental.model.Vehicle;
import com.carrental.model.VehicleStatus;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class RentalStore {
private String storeId;
private Location location;
private List<Vehicle> vehicles;
public RentalStore(String storeId, Location location) {
this.storeId = storeId;
this.location = location;
this.vehicles = new CopyOnWriteArrayList<>();
}
public void addVehicle(Vehicle vehicle) {
vehicles.add(vehicle);
}
public void removeVehicle(Vehicle vehicle) {
vehicles.remove(vehicle);
}
public List<Vehicle> getAvailableVehicles() {
List<Vehicle> availableVehicles = new ArrayList<>();
for (Vehicle v : vehicles) {
if (v.getStatus() == VehicleStatus.AVAILABLE) {
availableVehicles.add(v);
}
}
return availableVehicles;
}
public String getStoreId() { return storeId; }
public Location getLocation() { return location; }
public List<Vehicle> getVehicles() { return vehicles; }
}
9. Reservation Lifecycle & Management
User Entity
package com.carrental.model;
import java.util.ArrayList;
import java.util.List;
public class User {
private String userId;
private String name;
private String email;
private String driversLicenseNumber;
public User(String userId, String name, String email, String driversLicenseNumber) {
this.userId = userId;
this.name = name;
this.email = email;
this.driversLicenseNumber = driversLicenseNumber;
}
public String getUserId() { return userId; }
public String getName() { return name; }
}
Reservation Domain Model
package com.carrental.reservation;
import com.carrental.model.*;
import com.carrental.store.RentalStore;
import com.carrental.strategy.PricingStrategy;
import java.time.LocalDateTime;
public class Reservation {
private String reservationId;
private User user;
private Vehicle vehicle;
private RentalStore pickupStore;
private RentalStore dropStore;
private LocalDateTime startTime;
private LocalDateTime endTime;
private ReservationStatus status;
private double totalAmount;
public Reservation(String reservationId, User user, Vehicle vehicle, RentalStore pickupStore,
RentalStore dropStore, LocalDateTime startTime, LocalDateTime endTime,
PricingStrategy pricingStrategy) {
this.reservationId = reservationId;
this.user = user;
this.vehicle = vehicle;
this.pickupStore = pickupStore;
this.dropStore = dropStore;
this.startTime = startTime;
this.endTime = endTime;
this.status = ReservationStatus.PENDING;
this.totalAmount = pricingStrategy.calculatePrice(vehicle, startTime, endTime);
}
public void confirmReservation() {
this.status = ReservationStatus.CONFIRMED;
this.vehicle.setStatus(VehicleStatus.RESERVED);
}
public void startRental() {
this.status = ReservationStatus.IN_PROGRESS;
}
public void completeRental() {
this.status = ReservationStatus.COMPLETED;
this.vehicle.setStatus(VehicleStatus.AVAILABLE);
}
public void cancelReservation() {
this.status = ReservationStatus.CANCELLED;
this.vehicle.setStatus(VehicleStatus.AVAILABLE);
}
// Getters
public String getReservationId() { return reservationId; }
public User getUser() { return user; }
public Vehicle getVehicle() { return vehicle; }
public ReservationStatus getStatus() { return status; }
public double getTotalAmount() { return totalAmount; }
}
Reservation Manager
package com.carrental.reservation;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ReservationManager {
private Map<String, Reservation> reservationRegistry;
public ReservationManager() {
this.reservationRegistry = new ConcurrentHashMap<>();
}
public void addReservation(Reservation reservation) {
reservationRegistry.put(reservation.getReservationId(), reservation);
}
public Reservation getReservation(String reservationId) {
return reservationRegistry.get(reservationId);
}
public void confirmReservation(String reservationId) {
Reservation res = reservationRegistry.get(reservationId);
if (res != null) {
res.confirmReservation();
}
}
public void cancelReservation(String reservationId) {
Reservation res = reservationRegistry.get(reservationId);
if (res != null) {
res.cancelReservation();
}
}
public void completeRental(String reservationId) {
Reservation res = reservationRegistry.get(reservationId);
if (res != null) {
res.completeRental();
}
}
}
10. Payment Processing via Strategy Pattern
Strategy Interface & Concrete Implementations
package com.carrental.strategy;
public interface PaymentStrategy {
boolean processPayment(double amount);
}
package com.carrental.strategy;
public class CreditCardPaymentStrategy implements PaymentStrategy {
private String cardNumber;
private String cvv;
public CreditCardPaymentStrategy(String cardNumber, String cvv) {
this.cardNumber = cardNumber;
this.cvv = cvv;
}
@Override
public boolean processPayment(double amount) {
System.out.println("[Payment] Successfully processed Credit Card payment of $" + amount);
return true;
}
}
public class CashPaymentStrategy implements PaymentStrategy {
@Override
public boolean processPayment(double amount) {
System.out.println("[Payment] Collected Cash payment of $" + amount + " at store counter.");
return true;
}
}
public class PayPalPaymentStrategy implements PaymentStrategy {
private String email;
public PayPalPaymentStrategy(String email) {
this.email = email;
}
@Override
public boolean processPayment(double amount) {
System.out.println("[Payment] Successfully processed PayPal payment of $" + amount + " for account " + email);
return true;
}
}
Payment Processor Controller
package com.carrental.processor;
import com.carrental.strategy.PaymentStrategy;
public class PaymentProcessor {
public boolean executePayment(PaymentStrategy strategy, double amount) {
if (strategy == null) {
throw new IllegalArgumentException("Payment strategy must be specified.");
}
return strategy.processPayment(amount);
}
}
11. Orchestration via Singleton RentalSystem
The RentalSystem standardizes API interaction by acting as a thread-safe Singleton entry point.
package com.carrental.system;
import com.carrental.model.*;
import com.carrental.reservation.*;
import com.carrental.store.*;
import com.carrental.processor.PaymentProcessor;
import com.carrental.strategy.PaymentStrategy;
import com.carrental.strategy.PricingStrategy;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class RentalSystem {
private static RentalSystem instance;
private List<RentalStore> stores;
private List<User> users;
private ReservationManager reservationManager;
private PaymentProcessor paymentProcessor;
private RentalSystem() {
this.stores = new CopyOnWriteArrayList<>();
this.users = new CopyOnWriteArrayList<>();
this.reservationManager = new ReservationManager();
this.paymentProcessor = new PaymentProcessor();
}
public static synchronized RentalSystem getInstance() {
if (instance == null) {
instance = new RentalSystem();
}
return instance;
}
public void addStore(RentalStore store) {
stores.add(store);
}
public void addUser(User user) {
users.add(user);
}
public List<RentalStore> getStores() {
return stores;
}
public Reservation createReservation(User user, Vehicle vehicle, RentalStore pickupStore,
RentalStore dropStore, LocalDateTime start,
LocalDateTime end, PricingStrategy pricingStrategy) {
String reservationId = "RES-" + UUID.randomUUID().toString().substring(0, 8);
Reservation reservation = new Reservation(reservationId, user, vehicle, pickupStore, dropStore, start, end, pricingStrategy);
reservationManager.addReservation(reservation);
return reservation;
}
public boolean processReservationPayment(String reservationId, PaymentStrategy strategy) {
Reservation reservation = reservationManager.getReservation(reservationId);
if (reservation == null) {
System.out.println("Reservation not found!");
return false;
}
boolean success = paymentProcessor.executePayment(strategy, reservation.getTotalAmount());
if (success) {
reservationManager.confirmReservation(reservationId);
System.out.println("[System] Reservation " + reservationId + " confirmed successfully!");
}
return success;
}
public ReservationManager getReservationManager() {
return reservationManager;
}
}
12. Advanced Extensibility: Dynamic Pricing Strategy & Observer Pattern
To prevent architectural rigidity, business logic like pricing calculations and system alerts must be decoupled from core entities using dedicated patterns.
Decoupled Pricing Strategy Pattern
Instead of hardcoding cost calculations inside Vehicle models, we encapsulate temporal pricing logic within a PricingStrategy hierarchy:
package com.carrental.strategy;
import com.carrental.model.Vehicle;
import java.time.Duration;
import java.time.LocalDateTime;
public interface PricingStrategy {
double calculatePrice(Vehicle vehicle, LocalDateTime start, LocalDateTime end);
}
package com.carrental.strategy;
import com.carrental.model.Vehicle;
import java.time.Duration;
import java.time.LocalDateTime;
public class HourlyPricingStrategy implements PricingStrategy {
@Override
public double calculatePrice(Vehicle vehicle, LocalDateTime start, LocalDateTime end) {
long hours = Math.max(1, Duration.between(start, end).toHours());
return vehicle.calculateRentalFee(hours);
}
}
public class DailyPricingStrategy implements PricingStrategy {
@Override
public double calculatePrice(Vehicle vehicle, LocalDateTime start, LocalDateTime end) {
long days = Math.max(1, Duration.between(start, end).toDays());
// Apply a 10% daily discount factor
return vehicle.calculateRentalFee(days) * 24 * 0.90;
}
}
public class WeeklyPricingStrategy implements PricingStrategy {
@Override
public double calculatePrice(Vehicle vehicle, LocalDateTime start, LocalDateTime end) {
long weeks = Math.max(1, Duration.between(start, end).toDays() / 7);
// Apply a 25% bulk weekly discount factor
return vehicle.calculateRentalFee(weeks) * 24 * 7 * 0.75;
}
}
Observer Pattern for Event Notifications
The Observer Pattern decouples system updates (e.g., sending SMS alerts on booking, notifying waiting users of returned stock) from core domain classes.
package com.carrental.observer;
import com.carrental.reservation.Reservation;
public interface VehicleObserver {
void onReservationStatusChanged(Reservation reservation);
}
public class CustomerNotificationService implements VehicleObserver {
@Override
public void onReservationStatusChanged(Reservation reservation) {
System.out.println("[Notification Service] Email/SMS sent to "
+ reservation.getUser().getName()
+ ": Reservation " + reservation.getReservationId()
+ " is now " + reservation.getStatus());
}
}
13. End-to-End Execution Walkthrough
The following entry point demonstrates the end-to-end workflow: store setup, vehicle instantiation using the Factory, user registration, dynamic vehicle queries, reservation creation, pricing strategy invocation, and payment execution.
package com.carrental;
import com.carrental.factory.VehicleFactory;
import com.carrental.model.*;
import com.carrental.reservation.Reservation;
import com.carrental.store.RentalStore;
import com.carrental.strategy.*;
import com.carrental.system.RentalSystem;
import java.time.LocalDateTime;
import java.util.List;
public class Main {
public static void main(String[] args) {
System.out.println("==================================================");
System.out.println(" CAR RENTAL LOW-LEVEL DESIGN SYSTEM BOOTSTRAP ");
System.out.println("==================================================\n");
// 1. Initialize Singleton System Façade
RentalSystem rentalSystem = RentalSystem.getInstance();
// 2. Setup Stores & Locations
Location loc1 = new Location("123 Tech Park Rd", "Bangalore", "KA", "560103");
Location loc2 = new Location("456 Airport Rd", "Bangalore", "KA", "560017");
RentalStore storeCentral = new RentalStore("STORE-BLR-01", loc1);
RentalStore storeAirport = new RentalStore("STORE-BLR-02", loc2);
rentalSystem.addStore(storeCentral);
rentalSystem.addStore(storeAirport);
// 3. Create Vehicles via Factory
Vehicle ecoCar = VehicleFactory.createVehicle(VehicleType.ECONOMY, "KA-01-MA-1234", "Maruti", "Swift", 2022, 10.0);
Vehicle suvCar = VehicleFactory.createVehicle(VehicleType.SUV, "KA-05-MH-5678", "Tata", "Harrier", 2023, 25.0);
Vehicle luxCar = VehicleFactory.createVehicle(VehicleType.LUXURY, "KA-03-EQ-9999", "Mercedes", "C-Class", 2024, 80.0);
storeCentral.addVehicle(ecoCar);
storeCentral.addVehicle(suvCar);
storeAirport.addVehicle(luxCar);
// 4. Register Customer
User user = new User("USR-101", "John Doe", "john.doe@example.com", "DL-9876543210");
rentalSystem.addUser(user);
// 5. Query Available Vehicles at Central Store
System.out.println("--- Querying Available Vehicles at " + storeCentral.getStoreId() + " ---");
List<Vehicle> availableAtCentral = storeCentral.getAvailableVehicles();
for (Vehicle v : availableAtCentral) {
System.out.println("Found Vehicle: " + v.getMake() + " " + v.getModel()
+ " [" + v.getVehicleType() + "] - Base Rate: $" + v.getBaseRentalPrice() + "/hr");
}
System.out.println();
// 6. Create Reservation with Daily Pricing Strategy
LocalDateTime pickupTime = LocalDateTime.now().plusDays(1);
LocalDateTime dropTime = pickupTime.plusDays(3); // 3-Day Rental
PricingStrategy dailyPricing = new DailyPricingStrategy();
System.out.println("--- Initiating Reservation for " + suvCar.getModel() + " ---");
Reservation reservation = rentalSystem.createReservation(
user, suvCar, storeCentral, storeAirport, pickupTime, dropTime, dailyPricing
);
System.out.println("Reservation Generated ID: " + reservation.getReservationId());
System.out.println("Calculated Total Amount (3 Days Daily Strategy): $" + reservation.getTotalAmount());
System.out.println("Initial Reservation Status: " + reservation.getStatus());
System.out.println();
// 7. Execute Payment using Credit Card Strategy
System.out.println("--- Processing Payment ---");
PaymentStrategy creditCardPayment = new CreditCardPaymentStrategy("4111-2222-3333-4444", "123");
boolean paymentSuccess = rentalSystem.processReservationPayment(reservation.getReservationId(), creditCardPayment);
if (paymentSuccess) {
System.out.println("Updated Reservation Status: " + reservation.getStatus());
System.out.println("Vehicle Operational Status: " + suvCar.getStatus());
}
System.out.println();
// 8. Lifecycle Transitions: Start Rental and Complete Rental
System.out.println("--- Executing Rental Operations ---");
rentalSystem.getReservationManager().getReservation(reservation.getReservationId()).startRental();
System.out.println("Status after pickup: " + reservation.getStatus());
rentalSystem.getReservationManager().getReservation(reservation.getReservationId()).completeRental();
System.out.println("Status after return: " + reservation.getStatus());
System.out.println("Vehicle status after return: " + suvCar.getStatus());
System.out.println("\n==================================================");
System.out.println(" END-TO-END EXECUTION COMPLETED SUCCESSFULLY ");
System.out.println("==================================================");
}
}
14. Design Pattern Synthesis & Trade-offs
15. Summary and Key Takeaways
- System Boundary Clarity: Explicitly distinguish between point-to-point dynamic ride-hailing systems (Uber) and physical inventory checkout systems (Hertz) during initial requirement gathering.
- Encapsulated Creation: Implement a Factory Pattern (
VehicleFactory) to isolate object instantiation logic for diverse vehicle variants (Economy,Luxury,SUV), preventing client instantiation dependencies. - Single Orchestrator Façade: Structure the top-level API entry point as a Singleton (
RentalSystem) to manage execution states across underlying subsystem controllers cleanly. - Double Strategy Decoupling: Separate operational rules into isolated algorithms using the Strategy Pattern:
PaymentStrategy: Supports dynamic runtime selection of payment processing providers (Credit Card, Cash, PayPal).PricingStrategy: Decouples temporal cost calculations (Hourly, Daily, Weekly) from base domain entities.
- Explicit State Management: Use granular enumerations (
ReservationStatus,VehicleStatus,VehicleType) to manage operational transitions reliably across system states. - Observer-Driven Alerting: Discuss the Observer Pattern to decouple notification services (SMS/Email alerts, waiting-list notifications) from core reservation logic.
- Interview Time Management: Focus first on domain structures, abstract classes, core enums, and class diagrams. Implement main orchestration components next, leaving optional dynamic pricing and notification patterns as modular extensibility points.