🧠 Car Rental Platform LLD — Thought Process Guide¶
Goal: Learn how to think when designing a Low-Level Design.
📊 Class Diagram¶
classDiagram
direction LR
class CarRentalService {
+HOLD_TTL: timedelta
+MIN_RENTAL: timedelta
+RETURN_GRACE: timedelta
+POINTS_PER_HOUR: int
+calendar: AvailabilityCalendar
+search: SearchService
-_vehicles: Dict~str, Vehicle~
-_customers: Dict~str, Customer~
-_reservations: Dict~str, Reservation~
+add_vehicle(vehicle: Vehicle) None
+register_customer(name: str, email: str, license_number: str) Customer
+get_reservation(reservation_id: str) Reservation
+create_reservation(customer_id: str, vehicle_id: str, pickup: datetime, dropoff: datetime, ...) Reservation
+confirm_reservation(reservation_id: str) Reservation
+cancel_reservation(reservation_id: str) Reservation
+expire_holds() List~str~
+schedule_maintenance(vehicle_id: str, start: datetime, end: datetime) str
+start_rental(reservation_id: str) Reservation
+complete_rental(reservation_id: str, returned_at: datetime) Decimal
}
class Vehicle {
<<dataclass>>
+vehicle_id: str
+vehicle_type: VehicleType
+make: str
+model: str
+license_plate: str
+fuel_type: FuelType
+hourly_rate: Decimal
+daily_rate: Decimal
+location: str
+status: VehicleStatus
}
class Customer {
<<dataclass>>
+customer_id: str
+name: str
+email: str
+license_number: str
+loyalty_points: int
}
class Reservation {
<<dataclass>>
+reservation_id: str
+customer: Customer
+vehicle: Vehicle
+pickup: datetime
+dropoff: datetime
+pricing: RentalPricing
+quoted_amount: Decimal
+hold_expires_at: datetime
+status: ReservationStatus
+final_amount: Optional~Decimal~
+returned_at: Optional~datetime~
+duration_hours: int
+move_to(target: ReservationStatus) None
}
class AvailabilityCalendar {
-_turnaround: timedelta
-_schedules: Dict~str, VehicleSchedule~
+add_vehicle(vehicle_id: str) None
+try_block(vehicle_id: str, start: datetime, end: datetime, kind: BlockKind, ref_id: str, expires_at: datetime) bool
+confirm_hold(vehicle_id: str, ref_id: str) bool
+release(vehicle_id: str, ref_id: str) None
+is_available(vehicle_id: str, pickup: datetime, dropoff: datetime) bool
+get_available_vehicles(vehicle_ids: List~str~, pickup: datetime, dropoff: datetime) List~str~
+next_free_window(vehicle_id: str, pickup: datetime, dropoff: datetime) datetime
+free_hours(vehicle_id: str, day: date) List~int~
+get_availability_summary(vehicle_id: str, day: date) dict
+get_weekly_availability(vehicle_id: str, start_date: date) dict
+weekly_bitmap(vehicle_id: str, start_date: date) int
}
class VehicleSchedule {
+lock: Lock
-_blocks: List~Block~
+purge_expired(now: datetime) None
+conflict(start: datetime, end: datetime) Optional~Block~
+insert(block: Block) None
+find(ref_id: str) Optional~Block~
+remove(ref_id: str) None
+blocks() List~Block~
}
class Block {
<<dataclass>>
+start: datetime
+end: datetime
+kind: BlockKind
+ref_id: str
+expires_at: Optional~datetime~
+live(now: datetime) bool
}
class SearchService {
-_calendar: AvailabilityCalendar
+search_available(pickup: datetime, dropoff: datetime, vehicle_type: VehicleType, location: str) List~Vehicle~
+search_by_date(day: date, vehicle_type: VehicleType) dict
+browse_weekly(start_date: date, vehicle_type: VehicleType, location: str) dict
}
class RentalPricing {
<<abstract>>
+calculate_cost(vehicle: Vehicle, hours: int) Decimal
}
class HourlyRentalPricing {
+calculate_cost(vehicle: Vehicle, hours: int) Decimal
}
class DailyRentalPricing {
+calculate_cost(vehicle: Vehicle, hours: int) Decimal
}
class WeeklyDiscountPricing {
-_base: RentalPricing
+calculate_cost(vehicle: Vehicle, hours: int) Decimal
}
class ReservationStatus {
<<enumeration>>
PENDING
CONFIRMED
IN_PROGRESS
COMPLETED
CANCELLED
EXPIRED
}
class VehicleStatus {
<<enumeration>>
AVAILABLE
RENTED
}
class BlockKind {
<<enumeration>>
RESERVATION
MAINTENANCE
}
class VehicleType {
<<enumeration>>
HATCHBACK
SEDAN
SUV
LUXURY
VAN
}
class FuelType {
<<enumeration>>
PETROL
DIESEL
ELECTRIC
HYBRID
}
RentalPricing <|-- HourlyRentalPricing
RentalPricing <|-- DailyRentalPricing
RentalPricing <|-- WeeklyDiscountPricing
WeeklyDiscountPricing o-- RentalPricing : wraps
CarRentalService *-- AvailabilityCalendar
CarRentalService *-- SearchService
CarRentalService o-- "*" Vehicle
CarRentalService o-- "*" Customer
CarRentalService *-- "*" Reservation
SearchService --> AvailabilityCalendar : reads
SearchService ..> Vehicle
AvailabilityCalendar *-- "*" VehicleSchedule : one per vehicle
VehicleSchedule *-- "*" Block : sorted, non-overlapping
Block --> BlockKind
Reservation --> "1" Customer
Reservation --> "1" Vehicle
Reservation --> RentalPricing : quoted with
Reservation --> ReservationStatus
Vehicle --> VehicleType
Vehicle --> FuelType
Vehicle --> VehicleStatus
⏱️ How to Run This in a 45–60 min Interview¶
| Time | Step | What you do | What to say out loud |
|---|---|---|---|
| 0–7 | Clarify | Ask the questions below; write the agreed scope as a comment. | "The hard part here is when is a car free. I'll make that exact first and build the UI views on top." |
| 7–15 | Entities & interfaces | Vehicle, Customer, Reservation + status enum, RentalPricing ABC, the calendar, the facade. Sketch the reservation state machine. |
"A vehicle's future is a set of half-open intervals. Its status is only where it is right now." |
| 15–35 | Core code | VehicleSchedule.conflict() (overlap test), try_block, create_reservation, pricing, transitions table. |
"Two ranges [a,b) and [c,d) overlap iff a < d and c < b. Sorted, non-overlapping blocks make that a bisect." |
| 35–45 | Concurrency | Per-vehicle lock around check-and-insert; reservation lock around transitions. | "Search is a hint. The insert is the authority. In Postgres that's an exclusion constraint, not a SELECT-then-INSERT." |
| 45–55 | Extension | Holds with TTL, turnaround buffer, maintenance, late return, "any SUV". | "A hold is just a block with an expiry; maintenance is just a block of another kind." |
| 55–60 | Wrap up | Tests; how the hourly grid is served at scale. | "The 7×24 grid is a cached projection, rebuilt on write. Correctness never depends on the cache." |
Clarifying questions worth asking¶
- Granularity: can a booking start at 10:30, or only on the hour? (If any minute: store intervals, derive hours. Never round the stored booking.)
- Book a specific car or a class ("an SUV at the airport")? (Specific car is simpler; class means try candidates until one claims.)
- Payment flow: pay at booking, or hold then pay? How long is a hold? (Introduces
PENDINGwith TTL.) - Turnaround between rentals for cleaning? (Buffer on every block.)
- Pricing: hourly with a daily cap? Weekly/monthly discounts? Late-return policy and grace? (Strategy + decorator; store the quote.)
- Cancellation / no-show policy? (More transitions.)
- How far ahead can people book, and how far ahead do we display? (7-day display window ≠ booking horizon.)
- Concurrency: many users on the same car at peak (airport, Friday evening)? (Always yes.)
Phase 1: Identify the Nouns¶
"Customers search for cars free over a time range, hold one while paying, then pick up and return it. A car can never be promised to two people for overlapping time."
| Noun | Decision | Why |
|---|---|---|
| Vehicle | Dataclass | Type, rates, location, physical status. No subclass per type: the types differ only in data |
| Customer | Dataclass | Identity, loyalty points |
| Block | Dataclass | [start, end), kind (reservation / maintenance), optional hold expiry |
| VehicleSchedule | Class with a lock | Sorted non-overlapping blocks for one car; the unit of locking |
| AvailabilityCalendar | Class | All schedules, turnaround buffer, the hourly/weekly projections |
| Reservation | Dataclass with a lock | Lifecycle state machine; stores the quote |
| RentalPricing | ABC | Hourly, daily; discounts decorate |
| SearchService | Class | Read-side filtering and sorting |
| CarRentalService | Facade | Orchestrates booking and lifecycle |
Phase 2: Enums First¶
class VehicleType(Enum): HATCHBACK, SEDAN, SUV, LUXURY, VAN
class VehicleStatus(Enum): AVAILABLE, RENTED # physical state only
class ReservationStatus(Enum): PENDING, CONFIRMED, IN_PROGRESS, COMPLETED, CANCELLED, EXPIRED
class BlockKind(Enum): RESERVATION, MAINTENANCE
The trap: a RESERVED vehicle status. A car booked for next week is on the lot today. Future availability belongs in the calendar.
Phase 3: Assigning Responsibilities¶
| Action | Owner | Why |
|---|---|---|
| Overlap test | VehicleSchedule.conflict() |
Owns the sorted blocks |
| Atomic reserve | AvailabilityCalendar.try_block() |
Check + insert under the vehicle's lock |
| Hourly / weekly view | AvailabilityCalendar.free_hours() / get_weekly_availability() |
Projection of intervals |
| "When could I get it instead?" | AvailabilityCalendar.next_free_window() |
Walks the gaps once |
| Search | SearchService.search_available() |
Read side; no guarantees |
| Price | RentalPricing.calculate_cost(vehicle, hours) |
Strategy |
| Transitions | Reservation.move_to() |
Table-driven, raises on illegal moves |
| Orchestration | CarRentalService |
Hold, confirm, start, complete, cancel, expire |
Phase 4: The Core Data Structure¶
class VehicleSchedule:
def __init__(self):
self.lock = threading.Lock()
self._blocks: List[Block] = [] # sorted by start, never overlapping
def conflict(self, start, end) -> Optional[Block]:
i = bisect.bisect_right(self._blocks, start, key=lambda b: b.end)
if i < len(self._blocks) and self._blocks[i].start < end:
return self._blocks[i]
return None
Key insight: because blocks never overlap, sorting by start also sorts by end, so a single binary search finds the only block that could overlap. O(log n) check, O(n) insert (list), which is fine for the tens of bookings a car has in its horizon. An interval tree is only worth it when intervals may overlap (e.g. multi-unit inventory).
Why not hourly slots as the truth? They force rounding. Round down and you miss real overlaps; round up and you sell less. Store exact intervals; compute slots for display.
Phase 5: Concurrency¶
with sched.lock: # one lock per vehicle
if sched.conflict(s, e) is None:
sched.insert(Block(...)) # check and insert are one step
Same shape as the production answer: an EXCLUDE USING gist (vehicle_id WITH =, tstzrange(pickup, return) WITH &&) constraint makes the INSERT itself fail on overlap, at any isolation level.
Phase 6: Pricing (Strategy + Decorator)¶
class HourlyRentalPricing(RentalPricing): # each 24 h capped at the daily rate
class DailyRentalPricing(RentalPricing): # any started day = full day
class WeeklyDiscountPricing(RentalPricing): # wraps any strategy: -10% at 7 d, a further -15% at 30 d
Phase 7: Quick Checklist¶
✅ Exact intervals, half-open: back-to-back bookings allowed, sub-hour overlaps caught
✅ Atomic check-and-insert per vehicle; search is only a hint
✅ State machine: holds expire, illegal transitions raise
✅ Money: Decimal; quote stored on the reservation
✅ Time: injectable clock, so tests can move time forward