Design a Car Rental System
A full low-level design for a car rental platform: date-range availability checking, a reservation state machine, pricing with insurance and late fees, and damage-report handling.
Design a Car Rental System
Car Rental shares its hardest problem with Hotel Booking — date-range overlap detection under concurrency — but adds two wrinkles of its own: pricing composed from several independent add-ons (base rate, insurance, extras, late fee), and a post-rental damage-assessment flow that can retroactively change the final charge. This guide works the problem end-to-end using the six-step framework used throughout this phase: requirements → actors → class diagram → class design → patterns → algorithms/concurrency → trade-offs.
1. Requirements
Functional:
- Multiple car types (economy, sedan, SUV, luxury), each with its own daily rate.
- Search availability for a given car type and date range.
- Create a reservation for a specific car and date range; cancel a reservation.
- Process return: inspect for damage, calculate late fees if returned after the agreed end date, finalize the charge.
- Track customer history (useful for loyalty discounts).
Non-functional:
- Two reservations for the same physical car must never have overlapping date ranges — this is the correctness-under-concurrency core, same as Hotel Booking.
- Pricing must be composable: base rate + insurance + extras + late fee, each independently toggleable/configurable, without one component's logic reaching into another's.
2. Actors & Use Cases
| Actor | Use cases |
|---|---|
| Customer | Search availability, reserve a car, cancel, return a car |
| Rental Agent | Process return, file a damage report, override pricing (manager approval) |
| System | Compute price, detect date-range conflicts, calculate late fees |
3. Class Diagram
4. Core Class Design (Java)
DateRange overlap checking
record DateRange(LocalDate start, LocalDate end) {
boolean overlaps(DateRange other) {
return !this.end.isBefore(other.start) && !other.end.isBefore(this.start);
}
}Availability search — the same overlap-detection core as Hotel Booking
final class RentalStore {
List<Reservation> reservations;
List<Car> findAvailable(CarType type, DateRange requested) {
Set<String> bookedCarIds = reservations.stream()
.filter(r -> r.getStatus() != ReservationStatus.CANCELLED)
.filter(r -> r.getPeriod().overlaps(requested))
.map(r -> r.getCar().getId())
.collect(Collectors.toSet());
return cars.stream()
.filter(c -> c.getType() == type)
.filter(c -> !bookedCarIds.contains(c.getId()))
.toList();
}
}Concurrency: the same TOCTOU (time-of-check-to-time-of-use) gap from Hotel Booking applies here — two customers reserving the same available car simultaneously can both pass findAvailable before either commits. The fix is identical: make createReservation re-check overlap under a lock (row-level DB lock on the car, or a distributed lock keyed by car ID) at commit time, not just at search time.
Composable pricing via a component chain
interface PricingComponent {
Money apply(Money runningTotal, Reservation reservation);
}
final class BaseRateComponent implements PricingComponent {
public Money apply(Money runningTotal, Reservation r) {
long days = ChronoUnit.DAYS.between(r.getPeriod().start(), r.getPeriod().end());
return runningTotal.add(r.getCar().getDailyRate().multiply(days));
}
}
final class InsuranceComponent implements PricingComponent {
private final Money dailyInsuranceRate;
public Money apply(Money runningTotal, Reservation r) {
long days = ChronoUnit.DAYS.between(r.getPeriod().start(), r.getPeriod().end());
return runningTotal.add(dailyInsuranceRate.multiply(days));
}
}
final class LoyaltyDiscountComponent implements PricingComponent {
public Money apply(Money runningTotal, Reservation r) {
double discount = switch (r.getCustomer().getLoyaltyTier()) {
case 3 -> 0.15;
case 2 -> 0.10;
case 1 -> 0.05;
default -> 0.0;
};
return runningTotal.subtract(runningTotal.multiply(discount));
}
}
final class PricingCalculator {
Money calculate(Reservation reservation, List<PricingComponent> components) {
Money total = Money.ZERO;
for (PricingComponent component : components) {
total = component.apply(total, reservation);
}
return total;
}
}
// Usage: components are assembled per-reservation, e.g.
List<PricingComponent> components = List.of(
new BaseRateComponent(),
new InsuranceComponent(dailyInsuranceRate),
new LoyaltyDiscountComponent()
);
Money price = calculator.calculate(reservation, components);This is the Chain-of-Responsibility/Decorator-adjacent shape applied to pricing: each PricingComponent only knows how to adjust a running total, not about any other component. Adding a "weekend surcharge" or "one-way drop-off fee" later means writing one new class and adding it to the list — no existing component changes.
Reservation state machine
Return processing and late fees
final class ReturnProcessor {
Money processReturn(Reservation reservation, LocalDate actualReturnDate, DamageReport damage) {
Money lateFee = calculateLateFee(reservation.getPeriod().end(), actualReturnDate, reservation.getCar().getDailyRate());
Money damageCharge = damage != null ? damage.getAdditionalCharge() : Money.ZERO;
reservation.setStatus(ReservationStatus.RETURNED);
reservation.getCar().setStatus(CarStatus.AVAILABLE);
return lateFee.add(damageCharge);
}
private Money calculateLateFee(LocalDate agreedEnd, LocalDate actualReturn, Money dailyRate) {
long lateDays = ChronoUnit.DAYS.between(agreedEnd, actualReturn);
if (lateDays <= 0) return Money.ZERO;
long gracePeriodHours = 2; // e.g. first 2 hours late are free
Money perHourLateRate = dailyRate.divide(24).multiply(1.5); // 1.5x hourly rate for lateness
return perHourLateRate.multiply(lateDays * 24 - gracePeriodHours);
}
}5. Design Patterns Applied
| Pattern | Where used | Why |
|---|---|---|
| Strategy / Chain | PricingComponent list composing base rate + insurance + extras + discounts + late fee | New pricing rules plug in as new components, existing ones untouched |
| State | ReservationStatus (PENDING → CONFIRMED → ACTIVE → RETURNED, or CANCELLED) | Only valid transitions are allowed; return processing can't run on a non-ACTIVE reservation |
| Repository | RentalStore abstracts persistence of cars/reservations/customers | Testable without a real database |
6. Key Algorithms, Concurrency & Edge Cases
- Overlap detection: identical algorithm and identical concurrency fix as Hotel Booking's date-range checking — re-verify availability under a lock at reservation-commit time, not just at search time.
- Damage assessment timing: a
DamageReportis attached to aReservationafter return, meaningtotalCoston a reservation is not fully final until return processing completes — model this explicitly (e.g.estimatedCostat booking time vs.finalCostafter return) rather than mutating a single ambiguoustotalCostfield. - Grace period edge case: a customer returning 1 hour and 59 minutes late (inside the 2-hour grace period) should pay nothing —
calculateLateFeemust handle the "late but within grace period" case as zero, not a negative charge.
A subtle bug: computing late fees using ChronoUnit.DAYS.between truncates partial days. A car returned 30 minutes late should not be charged a full late day, and a car returned 25 hours late should be charged for just over 1 day, not exactly 1. Use a time-aware duration (Duration.between on full timestamps, not just LocalDate) if late fees need hour-level precision — the simplified version above is day-level for clarity.
7. Trade-offs & Extensions
| Decision | Trade-off |
|---|---|
PricingComponent list vs. a single calculatePrice() method | List is more code upfront, but every new pricing rule (surcharges, promo codes, corporate discounts) is additive, not a diff to a shared method |
Deferred finalCost until after return | More correct (damage/late fees genuinely aren't known at booking time), but requires callers to distinguish "estimated" vs. "final" cost everywhere it's displayed |
Extensions: one-way rentals (pickup at location A, return at location B) with a drop-off fee component; fleet maintenance scheduling (a car needs a buffer day between rentals for servicing — this changes availability search from "no overlap" to "no overlap plus buffer"); corporate/fleet accounts with centralized billing across many reservations.
Interview Questions
- How do you guarantee two customers can't both reserve the same car for overlapping dates?
- Walk through how the pricing system stays extensible as new charge types (surcharges, discounts) are added.
- Why is the final cost of a reservation not fully known until after the car is returned?
- How would you handle a customer returning a car late but within a grace period?
- How would you extend this design to support one-way rentals with different pickup and drop-off locations?