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
RentalCompanyoperates severalBranches, each holding a fleet ofVehicles. - A
Customersearches for vehicles of a given type available across all branches for a date range, and reserves one as aRentalAgreement. - 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.
- 1The customer asks the whole company, not a specific branch.
- 2The company fans the search out to every branch it operates.
- 3Each vehicle checks its own agreements for a date overlap - no shared state to query.
- 4A vehicle from the results is booked into a new agreement.
- 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
Code
Design decisions
- Availability is derived, not stored. A
boolean availablefield onVehiclewould 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 againstRentalAgreements 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 onRentalAgreementmeans it's tested once and never re-derived incorrectly. - Pricing is a
PricingStrategyper vehicle type, not anif/elseinBranch. 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 touchingRentalAgreementcreation. - 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.mdxandmovie-booking.mdx- two customers racing for the last convertible on Friday night must not both win.
Common follow-ups
- Why is availability derived from
RentalAgreementoverlap instead of a storedisAvailableflag? 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 -
reservechecksisAvailableand then callsaddAgreementas two separate steps, the same check-then-set race called out for seat locking inmovie-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'stodate 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
RentalAgreementassumes same-branch return already - it stores apickupBranchbut the return branch is implicit in this design. Add areturnBranchfield toRentalAgreement, and on return, move the vehicle's fleet membership from the pickupBranchto the returnBranch-Vehicle.isAvailabledoesn't change at all, since it never depended on which branch a vehicle "belongs to."
Check yourself
Why is `Vehicle.isAvailable` computed from its `RentalAgreement`s instead of read from a stored boolean field?