Car Rental System

Fleets, reservations, pickup/return, insurance, billing. State machine on the rental and a Strategy for tiered pricing.

System Intermediate
13 min read
ood case-study car-rental state-pattern strategy-pattern decorator

Context#

A car rental service maintains a fleet of vehicles across multiple branches, lets customers reserve a vehicle for a date range, hands over the keys at pickup, settles the bill on return, and applies insurance options on top of the base price. The problem is a very common OOD prompt because it sits next to Hotel Management and Movie Booking in shape — a finite inventory, a reservation lifecycle, and a billing surface that begs for both Strategy and Decorator.

The interviewer’s hidden objectives, roughly in order:

  • Can you model the rental’s lifecycle as a state machine without leaking the states into every other class?
  • Can you keep the pricing logic in one place — tiered by duration and customer class, with promo codes layered on?
  • Can you apply Decorator to insurance so a BaseCost becomes BaseCost + CDW + LDW + PAI without an if-chain?
  • Can you defend the separation between Reservation and Rental — a reservation is a promise, a rental is the kept promise?

Requirements (functional and non-functional)#

Clarifying scope is what keeps this a 45-minute problem.

Functional — in scope.

  • A fleet of vehicles across multiple branches. Each vehicle has a category (economy / compact / SUV / luxury / van).
  • Customers can search availability by branch, date range, and category.
  • Customers can reserve a vehicle; reservation holds the vehicle for the requested window.
  • At pickup, the reservation converts to an active rental. At return, the rental settles to a final bill.
  • Tiered pricing: daily, weekly, and weekend rates; corporate customers and promotional codes adjust the base.
  • Insurance options: Collision Damage Waiver (CDW), Loss Damage Waiver (LDW), Personal Accident Insurance (PAI). Each adds a per-day surcharge.
  • Late return charges extra; early return does not refund.
  • Damage assessment at return; if damage exceeds insurance coverage, the customer is billed for the excess.

Functional — out of scope (called out explicitly). Loyalty programs with tier benefits, multi-driver authorisation, one-way rentals between branches, fuel-level inspection beyond a simple full/empty flag, telematics, fleet maintenance scheduling. These will not be discussed unless added as follow-ups.

Non-functional.

  • Fleet sizes on the order of 10⁴ vehicles per region; reservations on the order of 10⁵ per month.
  • A reservation request resolves in well under 200 ms.
  • Concurrency: two customers must not be able to reserve the same vehicle for overlapping windows.
  • Persistence: stay in-process for the interview; flag the repository seams for future persistence.

Use case diagram#

┌────────────┐ ┌──────────────┐
│ Customer │ │ Branch Agent│
└──────┬─────┘ └──────┬───────┘
│ │
┌──────────┼─────────┐ │
▼ ▼ ▼ ▼
[search] [reserve] [pay] [hand over / receive]
│ │ │ │
│ │ │ [assess damage on return]
│ │ │ │
└──────────┴────┬────┴───────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ Car Rental System │
│ (inventory · reservations · billing · audit) │
└────────────────────────┬─────────────────────────┘
┌──────────────────────┐
│ Fleet Admin │ ── add/remove vehicles, tune rates
└──────────────────────┘

Class diagram#

┌──────────────────────────────┐
│ CarRentalSystem │
├──────────────────────────────┤
│ branches : List<Branch> │
│ rates : RateTable │
│ pricing : PricingStrategy │
│ payment : PaymentStrategy │
├──────────────────────────────┤
│ search(branch, range, cat) │
│ reserve(customer, vehicle, …)│
│ pickup(reservationId) │
│ returnVehicle(rentalId, …) │
└──────────────┬───────────────┘
│ 1..*
┌──────────────────────────────┐
│ Branch │
├──────────────────────────────┤
│ id, address │
│ vehicles : List<Vehicle> │
├──────────────────────────────┤
│ available(range, category) │
└──────────────┬───────────────┘
│ 1..*
┌──────────────────────────────┐
│ Vehicle │
├──────────────────────────────┤
│ vin, make, model │
│ category : Category │
│ branch │
└──────────────────────────────┘
┌─────────────────┐ ┌──────────────────────┐ ┌──────────────────────────┐
│ Customer │ │ Reservation │ │ Rental │
├─────────────────┤ ├──────────────────────┤ ├──────────────────────────┤
│ id, name │ │ id, customer │ │ id, reservation │
│ licence │ │ vehicle, branch │ │ pickedUpAt │
│ class : CustClass│ │ window : DateRange │ │ returnedAt? │
└─────────────────┘ │ status : RsvStatus │ │ damage : DamageReport? │
└──────────────────────┘ │ state : RentalState │
│ (Reserved/PickedUp/ │
│ Returned/Closed/ │
│ Cancelled) │
└────────────┬─────────────┘
┌──────────────────────────────────────┐ │
│ CostComponent │◁────────┘
│ (Decorator interface) │
├──────────────────────────────────────┤
│ total() : Money │
└──┬─────────────────┬─────────────────┘
│ │
┌───────────▼────────┐ ┌───▼────────────────┐ ┌──────────────────┐
│ BaseRentalCost │ │ InsuranceDecorator │ ──►│ CDW / LDW / PAI │
│ (PricingStrategy) │ │ (abstract wrapper) │ └──────────────────┘
└────────────────────┘ └─────────────────────┘
┌──────────────────────┐ ┌────────────────────┐
│ PricingStrategy │◁── │ PaymentStrategy │ ◁── Card / UPI / Cash
├──────────────────────┤ └────────────────────┘
│ Daily / Weekly / │
│ Weekend / Corporate /│
│ Promotional │
└──────────────────────┘

Three patterns share the load:

  • State pattern on RentalReserved → PickedUp → Returned → Closed | Cancelled. Each transition guards what the next operation may do.
  • Strategy pattern on PricingStrategy — the rate table feeds in, the strategy picks the right tier (daily / weekly / corporate / promotional). One place changes when ops releases a new promotion.
  • Decorator pattern on CostComponentBaseRentalCost is wrapped by CDW, LDW, PAI (or none) to compose the final bill. Adding a new insurance type is one new wrapper class.

What is deliberately not in the diagram:

  • No subclass per category. Category is an enum on Vehicle. Behaviour per category lives in pricing, not in a class hierarchy — the categories differ in price, not in interface.
  • Reservation and Rental are separate aggregates. A reservation may be cancelled before pickup; a rental may not “un-pickup.” Conflating them produces a state machine with too many holes.

Sequence diagram (key flows)#

The happy-path flow from search through return:

Customer System Branch Reservation Rental CostComponent
│ search(b,r,c)│ │ │ │ │
│─────────────►│ │ │ │ │
│ │ available(r,c) │ │ │
│ │─────────────►│ │ │ │
│ │ vehicles │ │ │ │
│ │◄─────────────│ │ │ │
│ list │ │ │ │ │
│◄─────────────│ │ │ │ │
│ reserve(v,r) │ │ │ │ │
│─────────────►│ │ │ │ │
│ │ new(v,r) ───────────────────────►│ │ │
│ │ │ │ Held │ │
│ rsv │ │ │ │ │
│◄─────────────│ │ │ │ │
│ pickup(rsv) │ │ │ │ │
│─────────────►│ │ │ │ │
│ │ │ │ convertToRental─────────────────►│
│ │ │ │ │
│ │ │ │ PickedUp │
│ return(rental, damage?) │ │ │
│─────────────►│ │ │ │ │
│ │ assessDamage ────────────────────────────────────►│ │
│ │ │ │ │ buildCost() │
│ │ │ │ │────────────────►│
│ │ │ │ │ Base + CDW + … │
│ │ │ │ │◄────────────────│
│ │ charge(payment, total) │ │ │
│ receipt │ │ │ │ Returned → Closed
│◄─────────────│ │ │ │ │

buildCost() is where the Decorator chain composes: new CDW(new LDW(new BaseRentalCost(rental, pricing))). The total is a single recursive call.

Activity diagram (for non-trivial state)#

The rental state machine — the spine of every guard in the system:

┌─────────┐
│ start │
└────┬────┘
┌────────────┐
│ Reserved │── customer no-show / cancel ────► ┌────────────┐
└────┬───────┘ │ Cancelled │
│ pickup └────────────┘
┌────────────┐
│ PickedUp │── damage event during use (logged but state unchanged)
└────┬───────┘
│ return (with optional damage report)
┌────────────┐
│ Returned │── assessment & payment ──► ┌────────────┐
└────────────┘ │ Closed │
└────────────┘

Closed is terminal. Reserved → Cancelled is the only legal exit before pickup. PickedUp → Cancelled does not exist — once the keys are out, you have to come back to Returned first.

Java implementation#

A representative slice — the rental state machine, the pricing strategy, and the insurance decorator chain.

public enum Category { ECONOMY, COMPACT, SUV, LUXURY, VAN }
public enum CustomerClass { RETAIL, CORPORATE }
public final class DateRange {
private final LocalDate start, end;
public DateRange(LocalDate s, LocalDate e) {
if (!e.isAfter(s)) throw new IllegalArgumentException("end must be after start");
this.start = s; this.end = e;
}
public long days() { return ChronoUnit.DAYS.between(start, end); }
public LocalDate start() { return start; }
public LocalDate end() { return end; }
}
public interface PricingStrategy {
Money price(Vehicle v, DateRange r, CustomerClass cls);
}
public final class TieredPricing implements PricingStrategy {
private final RateTable rates;
public TieredPricing(RateTable rates) { this.rates = rates; }
@Override public Money price(Vehicle v, DateRange r, CustomerClass cls) {
long days = r.days();
double dailyRate = rates.dailyFor(v.category(), cls);
if (days >= 7) {
// Weekly tier is cheaper per day; flatten partial weeks at the daily rate.
long weeks = days / 7;
long leftover = days % 7;
return Money.usd(weeks * rates.weeklyFor(v.category(), cls) + leftover * dailyRate);
}
return Money.usd(days * dailyRate);
}
}
public final class PromotionalPricing implements PricingStrategy {
private final PricingStrategy inner;
private final double percentOff;
public PromotionalPricing(PricingStrategy inner, double percentOff) {
this.inner = inner; this.percentOff = percentOff;
}
@Override public Money price(Vehicle v, DateRange r, CustomerClass cls) {
return inner.price(v, r, cls).times(1.0 - percentOff);
}
}
public interface CostComponent {
Money total();
}
public final class BaseRentalCost implements CostComponent {
private final Rental rental;
private final PricingStrategy pricing;
public BaseRentalCost(Rental r, PricingStrategy p) { this.rental = r; this.pricing = p; }
@Override public Money total() {
return pricing.price(rental.vehicle(), rental.window(), rental.customer().customerClass());
}
}
public abstract class InsuranceDecorator implements CostComponent {
protected final CostComponent inner;
protected InsuranceDecorator(CostComponent inner) { this.inner = inner; }
}
public final class CDW extends InsuranceDecorator {
public CDW(CostComponent inner) { super(inner); }
@Override public Money total() {
// Collision Damage Waiver: $15 per day
long days = ((BaseRentalCost) findBase(inner)).rental().window().days();
return inner.total().plus(Money.usd(days * 15.0));
}
private static CostComponent findBase(CostComponent c) {
while (c instanceof InsuranceDecorator d) c = d.inner;
return c;
}
}
public final class LDW extends InsuranceDecorator {
public LDW(CostComponent inner) { super(inner); }
@Override public Money total() {
long days = ((BaseRentalCost) CDW.findBase(inner)).rental().window().days();
return inner.total().plus(Money.usd(days * 10.0));
}
}
public final class Rental {
public enum State { RESERVED, PICKED_UP, RETURNED, CLOSED, CANCELLED }
private final String id;
private final Customer customer;
private final Vehicle vehicle;
private final DateRange window;
private Instant pickedUpAt;
private Instant returnedAt;
private DamageReport damage;
private State state = State.RESERVED;
public Rental(String id, Customer c, Vehicle v, DateRange w) {
this.id = id; this.customer = c; this.vehicle = v; this.window = w;
}
public void cancel() {
if (state != State.RESERVED) throw new IllegalStateException("Cannot cancel from " + state);
state = State.CANCELLED;
}
public void pickup(Instant at) {
if (state != State.RESERVED) throw new IllegalStateException("Cannot pick up from " + state);
this.pickedUpAt = at;
this.state = State.PICKED_UP;
}
public void markReturned(Instant at, DamageReport damage) {
if (state != State.PICKED_UP) throw new IllegalStateException("Cannot return from " + state);
this.returnedAt = at;
this.damage = damage;
this.state = State.RETURNED;
}
public void close() {
if (state != State.RETURNED) throw new IllegalStateException("Cannot close from " + state);
this.state = State.CLOSED;
}
public Customer customer() { return customer; }
public Vehicle vehicle() { return vehicle; }
public DateRange window() { return window; }
public State state() { return state; }
public Optional<DamageReport> damage() { return Optional.ofNullable(damage); }
}

At the use site, building the final bill is one expression:

CostComponent bill = new CDW(new LDW(new BaseRentalCost(rental, pricing)));
Money total = bill.total();

Notes interviewers reward:

  • Pricing is a chain. PromotionalPricing wraps TieredPricing — the promotion is itself a Strategy that decorates another Strategy. Composable without inheritance.
  • The Decorator is for insurance, not pricing. The two could be conflated; keeping them apart matches the org structure (revenue owns pricing, risk owns insurance).
  • require guards on Rental enforce the state machine. No client can call close() on a rental that has not been returned.
  • Customer is not subclassed by class — composition with an enum is the right call when behaviour difference is “different rate from the same table.”

Trade-offs and extensions#

DecisionWhyCost if requirements change
Reservation and Rental as separate aggregatesDistinct lifecycles, distinct invariantsSlight duplication in fields; the alternative (one class with eight states) is worse
CostComponent Decorator chainInsurance composes; no if-chainDecorator chain navigation (find-base) is ugly — wrap once and cache for hot paths
PricingStrategy interfacePromo / corporate / seasonal tiers all plug inNone — exactly the right shape
In-memory inventoryWithin scopeAdding persistence means repositories per aggregate; interface is the seam
Single coarse lock on reservationTwo reservations cannot collide on a vehicleAt national fleet scale, partition by branch
Damage as Optional<DamageReport> on RentalCommon case is no damage; the type makes presence explicitNone

Likely follow-up extensions:

  • One-way rentals. Add dropoffBranch to Rental; charge a one-way fee from a OneWayFee decorator. The fee depends on the branch pair, not on the customer.
  • Multiple drivers. Add additionalDrivers : List<Driver> to Rental; charge a per-driver-per-day fee via another Decorator.
  • Loyalty tier. Promote CustomerClass to a richer hierarchy or — better — keep the enum but tier the rate table. A PointsAccrual observer fires off the Rental.close() event.
  • Dynamic pricing. Replace the rate table with a service call; TieredPricing becomes a thin wrapper that reads live rates. The class structure is unchanged.
  • Fleet maintenance scheduling. Vehicle gains a MaintenanceSchedule; Branch.available filters out vehicles in service. A new aggregate (MaintenanceWindow) appears.

Mock interview follow-ups#

  • “Why Decorator for insurance and Strategy for pricing?” — Pricing is one decision (which tier?), Insurance is composable (zero, one, or many wraps). Strategy fits the first; Decorator fits the second. Using Decorator for pricing would force “no promotion” to be its own class — awkward.
  • “How do you prevent double-booking?” — Reservations are written through CarRentalSystem.reserve, which holds a lock or transaction over the inventory query and the write. At scale, partition by branch or vehicle.
  • “What happens on late return?” — The Rental’s markReturned records the actual return time. The cost calculation reads returnedAt - window.end and charges per-hour overage. Add a LateReturnSurcharge decorator if it grows complex.
  • “What’s the state machine?” — Five states: Reserved, PickedUp, Returned, Closed, Cancelled. Walk the activity diagram. Cancellation is only legal before pickup.
  • “How does this differ from the Hotel system?” — Same skeleton (inventory + reservations + billing + check-in/out state machine), different leaf entities. The pricing tiers are sharper here (corporate is a big segment); insurance is the only piece unique to rentals.
  • “Where would observers fit?” — On Rental state transitions, so loyalty, fraud, and analytics can subscribe without Rental knowing about them. Same as the Parking Lot exit-revenue observers.
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.