Car Rental
Strategy pattern for flexible pricing tiers and state pattern for vehicle lifecycle. Date-range availability search across a fleet of sedans, SUVs, and trucks.
Key Abstractions
Orchestrator. Handles reservations, returns, and fleet queries through a single entry point.
Entity with a type hierarchy (Sedan, SUV, Truck). Tracks plate, model, and current state.
Booking that binds a customer to a vehicle over a date range with a computed price.
Renter profile with name, license, and contact details.
Computes rental cost. Daily rate, weekly discount, insurance add-ons are all swappable strategies.
State machine: Available, Reserved, Rented, Maintenance. Controls which operations are legal.
Class Diagram
The Key Insight
A car rental system has two problems hiding behind one interface. The first is availability: figuring out which vehicles are free for a given date range. The second is pricing: computing the cost based on vehicle type, duration, and whatever promotions are running this week. If you merge these two concerns, you get a system that breaks every time marketing invents a new pricing tier.
The vehicle itself is a state machine. A car is not just "available" or "not available." It moves through Available, Reserved, Rented, and Maintenance. Each state constrains what operations are legal. You cannot rent a car that is under maintenance. You cannot cancel a reservation that has already been picked up. Making these transitions explicit eliminates an entire class of bugs that come from ad-hoc boolean flags.
Requirements
Functional
- Manage a fleet of vehicles across multiple types (Sedan, SUV, Truck)
- Search available vehicles for a given type and date range
- Create, pick up, return, and cancel reservations
- Calculate rental price based on vehicle type and duration
- Track vehicle state through its full lifecycle
Non-Functional
- Thread-safe reservation creation to prevent double-booking the same vehicle
- Pricing strategy must be swappable without touching reservation logic
- Date-range conflict detection must handle overlapping and adjacent reservations correctly
- Vehicle state transitions enforced by a state machine with no invalid jumps
Design Decisions
Why Strategy for pricing instead of a simple rate table?
A rate table works until it does not. Weekly discounts, weekend surcharges, loyalty programs, insurance add-ons, holiday premiums. Each of these is a different calculation. Strategy lets you compose and swap them. The reservation does not care how the number was computed. It just stores the result.
Why date-range overlap instead of a boolean availability flag?
A vehicle can be available right now but reserved for the weekend. A boolean tells you nothing about future availability. The overlap check (start_a < end_b AND start_b < end_a) is the correct way to determine if two time intervals conflict. Every booking system in production uses this formula.
Why a state machine for vehicle lifecycle?
Without explicit transitions, nothing prevents code from renting a vehicle that is under maintenance. The transition map (AVAILABLE can go to RESERVED or MAINTENANCE, RESERVED can go to RENTED or back to AVAILABLE) makes invalid operations fail immediately with a clear error. This is especially important because vehicles cycle through these states many times over their lifetime.
Interview Follow-ups
- "How would you handle late returns?" Compare actual return date against the reservation end date. Charge an overage rate (could be a separate PricingStrategy decorator) for the extra days.
- "How would you add insurance?" Insurance is an add-on pricing modifier. Wrap the base PricingStrategy in an InsurancePricingDecorator that adds the daily insurance premium to the base cost.
- "What about multiple locations?" Each location has its own fleet. A LocationService manages pickup and drop-off points. One-way rentals create a transfer record between locations.
- "How would you handle damage tracking?" Add a VehicleInspection entity linked to each reservation. Record damage at pickup and return. Disputes reference the inspection diff.
Code Implementation
1 from __future__ import annotations
2 from abc import ABC, abstractmethod
3 from enum import Enum
4 from datetime import date, timedelta
5 from dataclasses import dataclass, field
6 from typing import Protocol
7 from threading import Lock
8 import uuid
9
10
11 class VehicleType(Enum):
12 SEDAN = "sedan"
13 SUV = "suv"
14 TRUCK = "truck"
15
16
17 class VehicleState(Enum):
18 AVAILABLE = "available"
19 RESERVED = "reserved"
20 RENTED = "rented"
21 MAINTENANCE = "maintenance"
22
23
24 VALID_TRANSITIONS: dict[VehicleState, set[VehicleState]] = {
25 VehicleState.AVAILABLE: {VehicleState.RESERVED, VehicleState.MAINTENANCE},
26 VehicleState.RESERVED: {VehicleState.RENTED, VehicleState.AVAILABLE},
27 VehicleState.RENTED: {VehicleState.AVAILABLE},
28 VehicleState.MAINTENANCE: {VehicleState.AVAILABLE},
29 }
30
31
32 class ReservationStatus(Enum):
33 ACTIVE = "active"
34 PICKED_UP = "picked_up"
35 COMPLETED = "completed"
36 CANCELLED = "cancelled"
37
38
39 class Vehicle:
40 def __init__(self, vehicle_id: str, plate: str, model: str, vtype: VehicleType):
41 self.vehicle_id = vehicle_id
42 self.plate = plate
43 self.model = model
44 self.type = vtype
45 self.state = VehicleState.AVAILABLE
46
47 def transition_to(self, new_state: VehicleState) -> None:
48 allowed = VALID_TRANSITIONS.get(self.state, set())
49 if new_state not in allowed:
50 raise RuntimeError(
51 f"Cannot move {self.vehicle_id} from {self.state.value} to {new_state.value}"
52 )
53 self.state = new_state
54
55 def __repr__(self) -> str:
56 return f"{self.type.value.upper()}({self.plate}, {self.model})"
57
58
59 @dataclass
60 class Customer:
61 customer_id: str
62 name: str
63 license: str
64 email: str
65
66
67 class PricingStrategy(Protocol):
68 def calculate(self, vtype: VehicleType, start: date, end: date) -> float: ...
69
70
71 class DailyPricing:
72 DAILY_RATES = {
73 VehicleType.SEDAN: 40.0,
74 VehicleType.SUV: 65.0,
75 VehicleType.TRUCK: 80.0,
76 }
77
78 def calculate(self, vtype: VehicleType, start: date, end: date) -> float:
79 days = (end - start).days
80 if days <= 0:
81 raise ValueError("End date must be after start date")
82 return days * self.DAILY_RATES[vtype]
83
84
85 class WeeklyPricing:
86 """Charges a discounted weekly rate for full weeks, daily rate for leftover days."""
87 WEEKLY_RATES = {
88 VehicleType.SEDAN: 220.0,
89 VehicleType.SUV: 360.0,
90 VehicleType.TRUCK: 450.0,
91 }
92 DAILY_RATES = DailyPricing.DAILY_RATES
93
94 def calculate(self, vtype: VehicleType, start: date, end: date) -> float:
95 days = (end - start).days
96 if days <= 0:
97 raise ValueError("End date must be after start date")
98 weeks, remaining = divmod(days, 7)
99 return weeks * self.WEEKLY_RATES[vtype] + remaining * self.DAILY_RATES[vtype]
100
101
102 @dataclass
103 class Reservation:
104 reservation_id: str
105 customer: Customer
106 vehicle: Vehicle
107 start_date: date
108 end_date: date
109 total_price: float
110 status: ReservationStatus = ReservationStatus.ACTIVE
111
112 def overlaps(self, start: date, end: date) -> bool:
113 if self.status in (ReservationStatus.CANCELLED, ReservationStatus.COMPLETED):
114 return False
115 return self.start_date < end and start < self.end_date
116
117
118 class RentalSystem:
119 def __init__(self, vehicles: list[Vehicle], pricing: PricingStrategy | None = None):
120 self._vehicles = vehicles
121 self._pricing = pricing or DailyPricing()
122 self._reservations: dict[str, Reservation] = {}
123 self._lock = Lock()
124
125 def _has_conflict(self, vehicle: Vehicle, start: date, end: date) -> bool:
126 for res in self._reservations.values():
127 if res.vehicle.vehicle_id != vehicle.vehicle_id:
128 continue
129 if res.overlaps(start, end):
130 return True
131 return False
132
133 def search_available(self, vtype: VehicleType, start: date, end: date) -> list[Vehicle]:
134 available = []
135 for v in self._vehicles:
136 if v.type != vtype:
137 continue
138 if v.state == VehicleState.MAINTENANCE:
139 continue
140 if not self._has_conflict(v, start, end):
141 available.append(v)
142 return available
143
144 def reserve(self, customer: Customer, vehicle: Vehicle,
145 start: date, end: date) -> Reservation:
146 with self._lock:
147 if self._has_conflict(vehicle, start, end):
148 raise RuntimeError(f"{vehicle} is not available for {start} to {end}")
149 price = self._pricing.calculate(vehicle.type, start, end)
150 res_id = str(uuid.uuid4())[:8]
151 reservation = Reservation(
152 reservation_id=res_id,
153 customer=customer,
154 vehicle=vehicle,
155 start_date=start,
156 end_date=end,
157 total_price=price,
158 )
159 self._reservations[res_id] = reservation
160 vehicle.transition_to(VehicleState.RESERVED)
161 return reservation
162
163 def pick_up(self, reservation_id: str) -> None:
164 with self._lock:
165 res = self._reservations.get(reservation_id)
166 if res is None:
167 raise ValueError("Reservation not found")
168 if res.status != ReservationStatus.ACTIVE:
169 raise RuntimeError(f"Cannot pick up: reservation is {res.status.value}")
170 res.status = ReservationStatus.PICKED_UP
171 res.vehicle.transition_to(VehicleState.RENTED)
172
173 def return_vehicle(self, reservation_id: str) -> float:
174 with self._lock:
175 res = self._reservations.get(reservation_id)
176 if res is None:
177 raise ValueError("Reservation not found")
178 if res.status != ReservationStatus.PICKED_UP:
179 raise RuntimeError(f"Cannot return: reservation is {res.status.value}")
180 res.status = ReservationStatus.COMPLETED
181 res.vehicle.transition_to(VehicleState.AVAILABLE)
182 return res.total_price
183
184 def cancel_reservation(self, reservation_id: str) -> None:
185 with self._lock:
186 res = self._reservations.get(reservation_id)
187 if res is None:
188 raise ValueError("Reservation not found")
189 if res.status != ReservationStatus.ACTIVE:
190 raise RuntimeError(f"Cannot cancel: reservation is {res.status.value}")
191 res.status = ReservationStatus.CANCELLED
192 res.vehicle.transition_to(VehicleState.AVAILABLE)
193
194
195 if __name__ == "__main__":
196 vehicles = [
197 Vehicle("V1", "KA-01-1234", "Honda City", VehicleType.SEDAN),
198 Vehicle("V2", "KA-01-5678", "Toyota Camry", VehicleType.SEDAN),
199 Vehicle("V3", "KA-02-1111", "Ford Endeavour", VehicleType.SUV),
200 Vehicle("V4", "KA-03-2222", "Tata Ace", VehicleType.TRUCK),
201 ]
202
203 system = RentalSystem(vehicles, WeeklyPricing())
204 customer = Customer("C1", "Alice", "DL-9876", "alice@example.com")
205
206 today = date.today()
207 next_week = today + timedelta(days=7)
208 next_month = today + timedelta(days=30)
209
210 # Search for available sedans
211 available = system.search_available(VehicleType.SEDAN, today, next_week)
212 print(f"Available sedans ({today} to {next_week}): {available}")
213
214 # Reserve a sedan for one week
215 res1 = system.reserve(customer, vehicles[0], today, next_week)
216 print(f"Reserved {res1.vehicle} | Price: ${res1.total_price:.2f} | ID: {res1.reservation_id}")
217
218 # Check availability after reservation
219 available = system.search_available(VehicleType.SEDAN, today, next_week)
220 print(f"Available sedans after booking: {available}")
221
222 # Pick up and return
223 system.pick_up(res1.reservation_id)
224 print(f"Picked up {res1.vehicle} - state: {res1.vehicle.state.value}")
225
226 charge = system.return_vehicle(res1.reservation_id)
227 print(f"Returned {res1.vehicle} - charged: ${charge:.2f} - state: {res1.vehicle.state.value}")
228
229 # Reserve an SUV for 30 days, then cancel
230 res2 = system.reserve(customer, vehicles[2], today, next_month)
231 print(f"\nReserved {res2.vehicle} for 30 days | Price: ${res2.total_price:.2f}")
232
233 system.cancel_reservation(res2.reservation_id)
234 print(f"Cancelled reservation for {res2.vehicle} - state: {res2.vehicle.state.value}")Common Mistakes
- ✗Storing vehicle availability as a boolean. A vehicle can be available today but reserved for next week. You need date-range checks.
- ✗Putting pricing logic inside Reservation. That makes it impossible to swap pricing strategies or add promotional rates without touching bookings.
- ✗Skipping the state pattern and using string flags. You end up with scattered if-else blocks that miss edge cases like double-renting.
- ✗Not handling reservation cancellations. A cancelled reservation must transition the vehicle back to Available for that date range.
Key Points
- ✓Strategy pattern for pricing: daily, weekly, and promotional rates are interchangeable without modifying reservation logic
- ✓State pattern for vehicle lifecycle ensures illegal transitions (e.g., renting a vehicle under maintenance) are caught immediately
- ✓Observer notifies the fleet dashboard when vehicle state changes, keeping availability search results accurate
- ✓Date-range overlap check is the core of availability search. Two reservations conflict when start_a < end_b and start_b < end_a.