Overview
Design a car rental system. Requirements: branches have cars of different types (economy, SUV, luxury); customers search availability by branch, type, and date range; reserve a car; pick up and return it (possibly at a different branch); pay based on duration, car type, add-ons (insurance, GPS), and late fees; the system tracks each car's status and mileage.
Key modeling ideas: separate CarModel/CarType from individual Car (with license plate and status); a Reservation holds a date range, customer, and car; availability means no overlapping active reservation; pricing is a strategy (daily rate, weekend rate, add-ons as decorators or line items); and a reservation lifecycle state machine (reserved, picked up, returned, cancelled).
Like booking a hotel room, you reserve a type for certain dates, get a specific one at check-in, and pay for nights stayed plus extras. The desk must never give the same room to two guests on overlapping nights.
When to use it
- LLD interview for reservations with date ranges.
- Practicing overlap checks and pricing strategies.
- Similar to hotel rooms and equipment rentals.
Where it shows up in interviews
Recognize it when: book an item for a period without overlaps.
- Design a car rental system
- Design a hotel booking system
- Design a meeting room scheduler
Where it is used in real software
Fleet management with branch inventory, one-way rentals, and dynamic pricing.
Room types vs room numbers, overlapping stay checks.
The same interval overlap logic.
Key terms
- Car vs CarType
- Individual vehicle vs category with a rate.
- DateRange
- Value object with overlaps().
- Reservation
- Customer, car, range, status, add-ons.
- Late fee
- Charge for returning after the end date.
- One-way rental
- Return at a different branch.
How it works, step by step
- 1Clarify
Search filters, one-way returns, add-ons, late fees, cancellation.
- 2Entities
Branch, CarType, Car, Customer, Reservation, DateRange, PricingStrategy, AddOn.
- 3Availability
Cars of type at branch with no overlapping active reservation.
- 4Reserve atomically
Re-check overlap under lock.
- 5Pickup and return
Update status, mileage, branch, and compute final charge.
Overlap check
Car KA-01 reserved Jan 10 to Jan 13 (end exclusive)
| Requested range | Overlaps? | Reason |
|---|---|---|
| Jan 13 - Jan 15 | No | Starts when the other ends |
| Jan 12 - Jan 14 | Yes | Jan 12 is inside |
| Jan 8 - Jan 11 | Yes | Jan 10 is inside |
| Jan 5 - Jan 10 | No | Ends when the other starts |
NOWRequested range: Jan 13 - Jan 15 | Overlaps?: No | Reason: Starts when the other ends
Two half-open ranges overlap if startA < endB and startB < endA.
Implementation
import java.time.*;import java.util.*; enum CarType { ECONOMY(4000), SUV(7000), LUXURY(15000); final long dailyCents; CarType(long d) { dailyCents = d; } } record DateRange(LocalDate start, LocalDate end) { // end exclusive DateRange { if (!end.isAfter(start)) throw new IllegalArgumentException("end must be after start"); } boolean overlaps(DateRange o) { return start.isBefore(o.end) && o.start.isBefore(end); } long days() { return Duration.between(start.atStartOfDay(), end.atStartOfDay()).toDays(); }} final class Car { final String plate; final CarType type; String branch; int mileage; Car(String plate, CarType type, String branch) { this.plate = plate; this.type = type; this.branch = branch; }} final class Reservation { enum Status { RESERVED, PICKED_UP, RETURNED, CANCELLED } final String id = UUID.randomUUID().toString(); final Car car; final String customer; final DateRange range; final Set<String> addOns; Status status = Status.RESERVED; Reservation(Car car, String customer, DateRange range, Set<String> addOns) { this.car = car; this.customer = customer; this.range = range; this.addOns = addOns; } boolean active() { return status == Status.RESERVED || status == Status.PICKED_UP; }} interface PricingStrategy { long quote(CarType type, DateRange range, Set<String> addOns); } final class DailyPricing implements PricingStrategy { private static final Map<String, Long> ADD_ONS = Map.of("insurance", 1500L, "gps", 500L, "child-seat", 700L); public long quote(CarType type, DateRange r, Set<String> addOns) { long perDay = type.dailyCents + addOns.stream().mapToLong(a -> ADD_ONS.getOrDefault(a, 0L)).sum(); return perDay * r.days(); }} final class RentalService { private final List<Car> cars = new ArrayList<>(); private final Map<String, List<Reservation>> byPlate = new HashMap<>(); private final Map<String, Reservation> byId = new HashMap<>(); private final PricingStrategy pricing; private final long lateFeePerDayCents; RentalService(PricingStrategy pricing, long lateFeePerDayCents) { this.pricing = pricing; this.lateFeePerDayCents = lateFeePerDayCents; } void addCar(Car c) { cars.add(c); } synchronized List<Car> available(String branch, CarType type, DateRange range) { return cars.stream().filter(c -> c.branch.equals(branch) && c.type == type) .filter(c -> byPlate.getOrDefault(c.plate, List.of()).stream().noneMatch(r -> r.active() && r.range.overlaps(range))) .toList(); } synchronized Reservation reserve(String customer, String branch, CarType type, DateRange range, Set<String> addOns) { Car car = available(branch, type, range).stream().findFirst() .orElseThrow(() -> new IllegalStateException("No " + type + " available at " + branch)); Reservation r = new Reservation(car, customer, range, Set.copyOf(addOns)); byPlate.computeIfAbsent(car.plate, k -> new ArrayList<>()).add(r); byId.put(r.id, r); return r; } synchronized void pickUp(String id) { Reservation r = byId.get(id); if (r.status != Reservation.Status.RESERVED) throw new IllegalStateException("Cannot pick up in " + r.status); r.status = Reservation.Status.PICKED_UP; } synchronized long returnCar(String id, LocalDate returnedOn, String branch, int newMileage) { Reservation r = byId.get(id); if (r.status != Reservation.Status.PICKED_UP) throw new IllegalStateException("Car not picked up"); r.status = Reservation.Status.RETURNED; r.car.branch = branch; // one-way rentals supported r.car.mileage = newMileage; long lateDays = Math.max(0, Duration.between(r.range.end().atStartOfDay(), returnedOn.atStartOfDay()).toDays()); return pricing.quote(r.car.type, r.range, r.addOns) + lateDays * lateFeePerDayCents; } synchronized void cancel(String id) { Reservation r = byId.get(id); if (r.status != Reservation.Status.RESERVED) throw new IllegalStateException("Only reserved bookings can be cancelled"); r.status = Reservation.Status.CANCELLED; }}Complexity and performance
Index by car and date for scale.
Two comparisons.
Trade-offs
Assigning at pickup maximizes fleet flexibility; assigning at booking simplifies availability but fragments inventory.
A strategy interface lets you start with daily rates and later plug in demand-based pricing.
Variants and related techniques
Faster overlap queries with many reservations.
Same design with rooms instead of cars.
Common mistakes
- Inclusive end dates causing off-by-one overlaps.
Fix: Use half-open ranges [start, end).
- Checking availability and reserving without a lock.
Fix: Re-check under the same lock that creates the reservation.
- Status as free text.
Fix: Enum with guarded transitions.
Interview questions
How do you check if a car is available for a date range?
A car is available if none of its active reservations overlap the requested half-open range, where overlap means requested.start < existing.end and existing.start < requested.end.
How do you support one-way rentals?
Update the car's branch on return, and include a drop-off fee strategy; availability queries then use the car's current branch and future reservations' pickup branches.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| My Calendar I (LeetCode 729) | Medium | Interval overlap. |
| Car rental with add-ons and late fees | Medium | Pricing strategy. |