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.

Common follow-ups

  • Why is availability derived from RentalAgreement overlap instead of a stored isAvailable flag? A flag needs updating at both the start and end of every agreement, and any missed update - a bug, a crash mid-transaction - leaves it lying about reality indefinitely. Deriving it from the agreements themselves means there's nothing to keep in sync: the agreements are the only source of truth, so a query is always correct by construction rather than correct as long as every write path remembered to update the flag.
  • Two customers try to book the last convertible for the same weekend at nearly the same instant - what stops both from succeeding? As written, nothing does - reserve checks isAvailable and then calls addAgreement as two separate steps, the same check-then-set race called out for seat locking in movie-booking.mdx. Fixing it needs the check-and-append to be one atomic operation, guarded by a lock scoped to that specific vehicle (not the whole fleet, or every customer browsing other cars gets serialized too).
  • A customer wants to extend their rental by two days while already holding the car - how does that fit this model? It's not a mutation of the existing RentalAgreement; it's a new availability check for the extension window plus a new agreement (or an amendment method that re-validates overlap against every other agreement on that vehicle, excluding the one being extended). Mutating the original agreement's to date in place would break the "agreements are the single source of truth" invariant the whole availability check depends on.
  • How would you let a customer pick up in one city and drop off in another? Nothing about RentalAgreement assumes same-branch return already - it stores a pickupBranch but the return branch is implicit in this design. Add a returnBranch field to RentalAgreement, and on return, move the vehicle's fleet membership from the pickup Branch to the return Branch - Vehicle.isAvailable doesn't change at all, since it never depended on which branch a vehicle "belongs to."

Check yourself

Question 1 of 3

Why is `Vehicle.isAvailable` computed from its `RentalAgreement`s instead of read from a stored boolean field?