Skip to main content

Car Rental System

The trick to this one isn't renting a car - it's that "available" depends on a date range, not a boolean. A vehicle sitting idle today can still be unavailable for the week someone wants it, because it's already booked for three of those seven days.

Requirements

Functional

  • A RentalCompany operates several Branches, each holding a fleet of Vehicles.
  • A Customer searches for vehicles of a given type available across all branches for a date range, and reserves one as a RentalAgreement.
  • A vehicle already booked for any overlapping date range must not be offered again for that range.
  • Returning a vehicle (ending the agreement) can happen at a different branch than it was picked up from.

Non-functional

  • Availability search must not be a per-vehicle linear scan of every agreement ever made once history grows large - the design should call out where an index goes even if it keeps the reference implementation simple.
  • Price depends on both vehicle type and rental duration (weekly rates undercut day-rate multiples), and that formula needs to change without touching search or booking code.

Design

A Vehicle doesn't track "available" as a field at all - availability is computed by asking whether any of its existing RentalAgreements overlap the requested range. Branch searches its own fleet; RentalCompany fans that search out across branches. Pricing is delegated to a PricingStrategy keyed by vehicle type, the same shape as the fee strategy in parking-lot.mdx, because "how much does this cost" is exactly the kind of rule that changes on its own schedule.

CustomerRentalCompanyBranchVehiclePricingStrategysearch(type, from, to)1findAvailable(type, from, to)2isAvailable(from, to)3reserve(vehicle, from, to)4quote(vehicle.type, days)5
  1. 1The customer asks the whole company, not a specific branch.
  2. 2The company fans the search out to every branch it operates.
  3. 3Each vehicle checks its own agreements for a date overlap - no shared state to query.
  4. 4A vehicle from the results is booked into a new agreement.
  5. 5Price is computed by a strategy keyed on type - the company never hardcodes a rate.

RentalAgreement is the record of one booking - vehicle, customer, pickup/return branch, and date range - and it's the only thing consulted when checking whether a vehicle is free, never a separate availableDates cache that could drift out of sync with it.

Class diagram

«interface»PricingStrategy+ quote(days): doubleRentalCompany- branches: List<Branch>- pricing: Map<VehicleType, PricingStrategy>+ search(type, from, to): List<Vehicle>+ reserve(vehicle, customer, from, to): RentalAgreementBranch- id: string- fleet: List<Vehicle>+ findAvailable(type, from, to): List<Vehicle>Vehicle- id: string- type: VehicleType- agreements: List<RentalAgreement>+ isAvailable(from, to): boolCustomer- id: string- name: string- licenseNumber: stringRentalAgreement- vehicle: Vehicle- customer: Customer- pickupBranch: Branch- from: date- to: date- price: double+ overlaps(from, to): boolDailyRatePricing+ quote(days): doubleWeeklyDiscountPricing+ quote(days): double
implementsuses
RentalCompany fans a search across Branches; each Vehicle's availability is derived from its own RentalAgreements, not a stored flag.

Code

import java.time.LocalDate;
import java.util.*;
 
enum VehicleType { ECONOMY, SUV, LUXURY }
 
class Customer {
final String id;
final String name;
 
Customer(String id, String name) {
this.id = id;
this.name = name;
}
}
 
class RentalAgreement {
final Vehicle vehicle;
final Customer customer;
final LocalDate from;
final LocalDate to;
final double price;
 
RentalAgreement(Vehicle vehicle, Customer customer, LocalDate from, LocalDate to, double price) {
this.vehicle = vehicle;
this.customer = customer;
this.from = from;
this.to = to;
this.price = price;
}
 
boolean overlaps(LocalDate otherFrom, LocalDate otherTo) {
return from.isBefore(otherTo) && otherFrom.isBefore(to);
}
}
 
class Vehicle {
final String id;
final VehicleType type;
private final List<RentalAgreement> agreements = new ArrayList<>();
 
Vehicle(String id, VehicleType type) {
this.id = id;
this.type = type;
}
 
boolean isAvailable(LocalDate from, LocalDate to) {
return agreements.stream().noneMatch(a -> a.overlaps(from, to));
}
 
void addAgreement(RentalAgreement agreement) {
agreements.add(agreement);
}
}
 
interface PricingStrategy {
double quote(long days);
}
 
class DailyRatePricing implements PricingStrategy {
private final double dailyRate;
 
DailyRatePricing(double dailyRate) {
this.dailyRate = dailyRate;
}
 
public double quote(long days) {
return dailyRate * days;
}
}
 
class WeeklyDiscountPricing implements PricingStrategy {
private final double dailyRate;
private final double weeklyDiscountPercent;
 
WeeklyDiscountPricing(double dailyRate, double weeklyDiscountPercent) {
this.dailyRate = dailyRate;
this.weeklyDiscountPercent = weeklyDiscountPercent;
}
 
public double quote(long days) {
double base = dailyRate * days;
if (days >= 7) base *= (1 - weeklyDiscountPercent / 100.0);
return base;
}
}
 
class Branch {
final String id;
private final List<Vehicle> fleet;
 
Branch(String id, List<Vehicle> fleet) {
this.id = id;
this.fleet = fleet;
}
 
List<Vehicle> findAvailable(VehicleType type, LocalDate from, LocalDate to) {
return fleet.stream()
.filter(v -> v.type == type && v.isAvailable(from, to))
.toList();
}
}
 
class RentalCompany {
private final List<Branch> branches;
private final Map<VehicleType, PricingStrategy> pricing;
 
RentalCompany(List<Branch> branches, Map<VehicleType, PricingStrategy> pricing) {
this.branches = branches;
this.pricing = pricing;
}
 
List<Vehicle> search(VehicleType type, LocalDate from, LocalDate to) {
List<Vehicle> results = new ArrayList<>();
for (Branch branch : branches) results.addAll(branch.findAvailable(type, from, to));
return results;
}
 
RentalAgreement reserve(Vehicle vehicle, Customer customer, LocalDate from, LocalDate to) {
if (!vehicle.isAvailable(from, to)) {
throw new IllegalStateException("Vehicle no longer available for that range");
}
long days = java.time.temporal.ChronoUnit.DAYS.between(from, to);
double price = pricing.get(vehicle.type).quote(days);
RentalAgreement agreement = new RentalAgreement(vehicle, customer, from, to, price);
vehicle.addAgreement(agreement);
return agreement;
}
}

Design decisions

  • Availability is derived, not stored. A boolean available field on Vehicle would need updating at both the start and end of every agreement, and any missed update - a bug, a crash mid-transaction - leaves the field lying about reality. Computing it from the overlap check against RentalAgreements means there's nothing to keep in sync; the agreements are the single source of truth.
  • Overlap is one date-range comparison (aStart < bEnd && bStart < aEnd), reused everywhere availability is checked. Writing that comparison inline at each call site is exactly the kind of off-by-one that gets an interviewee marked down; giving it one home on RentalAgreement means it's tested once and never re-derived incorrectly.
  • Pricing is a PricingStrategy per vehicle type, not an if/else in Branch. Weekly discounts, seasonal surcharges, and loyalty rates are business decisions that change far more often than the booking flow itself; isolating them means marketing can change a rate table without anyone touching RentalAgreement creation.
  • What's missing for a real system: the linear overlap scan over a vehicle's agreements is fine at demo scale but needs an interval tree or a date-bucketed index once a fleet has years of booking history, and reserving a vehicle across a distributed system needs the same atomic check-and-book guarantee called out in parking-lot.mdx and movie-booking.mdx - two customers racing for the last convertible on Friday night must not both win.
0%0 of 122 pages studied