06-lld-interview-problems

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.

August 11, 2026
lldcar-rentalreservationdate-rangepricingstate-machine

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

ActorUse cases
CustomerSearch availability, reserve a car, cancel, return a car
Rental AgentProcess return, file a damage report, override pricing (manager approval)
SystemCompute price, detect date-range conflicts, calculate late fees

3. Class Diagram


4. Core Class Design (Java)

DateRange overlap checking

java
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

java
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

java
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

java
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

PatternWhere usedWhy
Strategy / ChainPricingComponent list composing base rate + insurance + extras + discounts + late feeNew pricing rules plug in as new components, existing ones untouched
StateReservationStatus (PENDING → CONFIRMED → ACTIVE → RETURNED, or CANCELLED)Only valid transitions are allowed; return processing can't run on a non-ACTIVE reservation
RepositoryRentalStore abstracts persistence of cars/reservations/customersTestable 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 DamageReport is attached to a Reservation after return, meaning totalCost on a reservation is not fully final until return processing completes — model this explicitly (e.g. estimatedCost at booking time vs. finalCost after return) rather than mutating a single ambiguous totalCost field.
  • Grace period edge case: a customer returning 1 hour and 59 minutes late (inside the 2-hour grace period) should pay nothing — calculateLateFee must 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

DecisionTrade-off
PricingComponent list vs. a single calculatePrice() methodList 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 returnMore 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?