Car Rental System
Fleets, reservations, pickup/return, insurance, billing. State machine on the rental and a Strategy for tiered pricing.
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
BaseCostbecomesBaseCost + CDW + LDW + PAIwithout anif-chain? - Can you defend the separation between
ReservationandRental— 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
Rental—Reserved → 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
CostComponent—BaseRentalCostis wrapped byCDW,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.
Categoryis an enum onVehicle. Behaviour per category lives in pricing, not in a class hierarchy — the categories differ in price, not in interface. ReservationandRentalare 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.
PromotionalPricingwrapsTieredPricing— 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).
requireguards onRentalenforce the state machine. No client can callclose()on a rental that has not been returned.Customeris 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#
| Decision | Why | Cost if requirements change |
|---|---|---|
Reservation and Rental as separate aggregates | Distinct lifecycles, distinct invariants | Slight duplication in fields; the alternative (one class with eight states) is worse |
CostComponent Decorator chain | Insurance composes; no if-chain | Decorator chain navigation (find-base) is ugly — wrap once and cache for hot paths |
PricingStrategy interface | Promo / corporate / seasonal tiers all plug in | None — exactly the right shape |
| In-memory inventory | Within scope | Adding persistence means repositories per aggregate; interface is the seam |
| Single coarse lock on reservation | Two reservations cannot collide on a vehicle | At national fleet scale, partition by branch |
Damage as Optional<DamageReport> on Rental | Common case is no damage; the type makes presence explicit | None |
Likely follow-up extensions:
- One-way rentals. Add
dropoffBranchtoRental; charge a one-way fee from aOneWayFeedecorator. The fee depends on the branch pair, not on the customer. - Multiple drivers. Add
additionalDrivers : List<Driver>toRental; charge a per-driver-per-day fee via another Decorator. - Loyalty tier. Promote
CustomerClassto a richer hierarchy or — better — keep the enum but tier the rate table. APointsAccrualobserver fires off theRental.close()event. - Dynamic pricing. Replace the rate table with a service call;
TieredPricingbecomes a thin wrapper that reads live rates. The class structure is unchanged. - Fleet maintenance scheduling.
Vehiclegains aMaintenanceSchedule;Branch.availablefilters 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
markReturnedrecords the actual return time. The cost calculation readsreturnedAt - window.endand charges per-hour overage. Add aLateReturnSurchargedecorator 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
Rentalstate transitions, so loyalty, fraud, and analytics can subscribe withoutRentalknowing about them. Same as the Parking Lot exit-revenue observers.
Related#
- Hotel Management System — same skeleton; differs in leaf entities and on the housekeeping subdomain.
- Decorator Pattern — insurance is the textbook application.
- State Pattern — rental lifecycle is the canonical state machine.
- Strategy Pattern — tiered pricing is the textbook application.
- Approaching the OOD Interview — the meta-script that shaped this writeup.