Design a car rental system¶
TL;DR¶
- You build a multi-branch rental company: customers search by city and dates, reserve a class of car, and are handed a plate only at the desk.
- Three decisions carry the interview: half-open date intervals so overlap detection is one comparison, counted availability per class plus a per-vehicle calendar so the two never double-count, and one lock per branch so two cities never contend.
- Patterns that earn their place: Strategy (rate plans), Decorator (add-ons, because insurance prices itself off what it wraps), Object Pool (the fleet), Factory Method (vehicles), Facade (
RentalSystem).
Problem statement¶
"Design the software behind a car rental company. It has branches in several cities, each with a fleet of vehicles of different classes. Customers search by city, dates and class, reserve a car, pick it up, drive it, and drop it off — sometimes at a different branch. You bill a daily or weekly rate plus optional extras, and you charge for late returns, missing fuel and damage. Cars also need scheduled maintenance. Model the classes, the booking flow, and what happens when two customers want the last car."
Requirements¶
Functional
- Branches in multiple cities; each owns a fleet of vehicles with a class, a plate and a status.
- Search by city, date range and vehicle class; the result is how many cars are free for the whole range.
- Reserve a class for a date range with a pickup branch and a drop-off branch (which may differ).
- Pricing: a daily rate per class, a weekly rate for stays of seven days or more, and add-ons (GPS, child seat, insurance).
- Pickup pins a physical car to the reservation and records odometer and fuel; return records them again.
- Charges on return: base rate, add-ons, late days, refuelling, mileage above the allowance, one-way fee, damage.
- Cancellation, with a fee inside the free-cancellation window; a no-show releases the hold.
- Maintenance windows are scheduled per vehicle and remove it from availability for those days.
Non-functional and constraints
- Never oversell a branch: if three SUVs are on the lot, at most three overlapping reservations exist for that week.
- A vehicle can never hold two overlapping bookings, including maintenance.
- In-memory and single-process; repositories are the seam where a database would go.
- Deterministic: the clock and the ID generators are injected, so every test pins a real calendar date.
Out of scope: loyalty programmes, dynamic pricing, telematics, the payment gateway beyond a processor interface, and driver licence verification.
Clarifying questions and assumptions¶
| Question to ask | Assumption taken here |
|---|---|
| Does a customer book a specific car or a class of car? | A class. Every real rental company does this, and it is the source of the most interesting logic on this page. |
| What is the unit of time — a day or an hour? | A calendar day, modelled as a half-open range [start, end). Hourly rentals are the same code with a different resolution. |
| Can a rental start and end at different branches? | Yes. One-way rentals attract a fee and physically move the car between fleets. |
| What happens when the booked class is out at the desk? | The desk walks an upgrade ladder and bills the class the customer booked. A van has no substitute, so a van booking can only be filled by a van. |
| Is the reservation confirmed by payment? | No. The hold is free; money is taken at return. This keeps the payment saga out of the core flow, and you can add an authorisation later. |
| Do branches share a fleet? | Not for availability. A car belongs to exactly one branch at a time; rebalancing is an explicit transfer. |
| How precise must overlap detection be? | Exact. Two bookings on one plate that share a single day is a bug that puts two customers in the same car. |
Core entities and relationships¶
- DateRange — a frozen half-open interval
[start, end)withoverlapsanddays. Everything about time flows through it. - Vehicle (abstract) with
EconomyCar,Sedan,Suv,Van,LuxuryCar. Each subclass fixes itsvehicle_typeandseats;VehicleFactoryturns a fleet-import string into the right subclass. - VehicleCalendar — the blocked ranges of one car: its rentals and its maintenance windows. One vehicle owns exactly one calendar.
- FleetPool — one branch's cars, with
acquireandrelease. This is the Object Pool: the desk borrows a car, it never creates one. - Branch — a location: a
FleetPool, a ledger of open holds, and the lock that makes both consistent. One branch, many vehicles; one branch, many reservations. - Reservation — customer, class, branches,
DateRange, add-ons, and a status that walksRESERVED → PICKED_UP → RETURNED. It carries a plate only after pickup. - RatePlan (
DailyRate,WeeklyRate) and AddOn (GpsAddOn,ChildSeatAddOn,InsuranceAddOn) — the pricing policies; ReturnCharges prices what only the return desk knows. - Invoice / InvoiceLine / Payment — an itemised bill, because a customer who is charged 1,098.60 USD wants seven lines, not one number.
- RentalSystem — the facade the API layer talks to: search, reserve, cancel, pick up, return, pay, schedule maintenance.
Multiplicities: company 1 → * branches, branch 1 → * vehicles, vehicle 1 → 1 calendar, branch 1 → * reservations, reservation 0..1 → 1 vehicle (pinned at pickup), reservation 1 → * invoice lines.
Class diagram¶
Structure: branches own cars, cars own calendars, reservations own dates.
classDiagram
class DateRange {
+start: date
+end: date
+days int
+overlaps(other) bool
+extended_to(new_end) DateRange
}
class VehicleCalendar {
+is_free(period) bool
+conflict(period) str
+block(label, period)
+block_or_merge(label, period) str
+extend(label, new_end) List~str~
}
class Vehicle {
<<abstract>>
+plate: str
+branch_id: str
+status: VehicleStatus
+odometer_km: int
+vehicle_type VehicleType
+seats int
}
class EconomyCar
class Sedan
class Suv
class Van
class LuxuryCar
class VehicleFactory {
+create(vehicle_type, plate, branch_id) Vehicle
}
class FleetPool {
+acquire(classes, period, label) Vehicle
+release(plate, needs_service, today) Vehicle
+serviceable(vehicle_type) List~Vehicle~
}
class Branch {
+id: str
+city: str
+lock: RLock
+available(vehicle_type, period) int
+hold(reservation)
+check_out(reservation) Vehicle
+check_in(reservation, return_date, needs_service) tuple
}
class Reservation {
+id: str
+vehicle_type: VehicleType
+period: DateRange
+status: ReservationStatus
+plate: str
+late_days int
}
class MaintenanceRecord {
+plate: str
+period: DateRange
+reason: str
}
class Customer {
+id: str
+licence_number: str
}
class RentalSystem {
+search(city, vehicle_type, period) List~str~
+reserve(customer_id, vehicle_type, branch, period) Reservation
+pick_up(reservation_id) Vehicle
+return_vehicle(reservation_id, return_date, odometer_km, fuel_eighths) Invoice
}
Vehicle <|-- EconomyCar
Vehicle <|-- Sedan
Vehicle <|-- Suv
Vehicle <|-- Van
Vehicle <|-- LuxuryCar
VehicleFactory ..> Vehicle : creates
Vehicle "1" *-- "1" VehicleCalendar
VehicleCalendar o-- "many" DateRange
Branch "1" *-- "1" FleetPool
FleetPool "1" o-- "many" Vehicle
RentalSystem "1" o-- "many" Branch
RentalSystem "1" o-- "many" Reservation
Reservation --> DateRange
Branch ..> MaintenanceRecord
Reservation --> Customer
Pricing: one rate plan, wrapped once per add-on, plus what the return desk discovers.
classDiagram
class RatePlan {
<<interface>>
+price(vehicle_type, days) tuple
}
class DailyRate {
+rate_for(vehicle_type) Money
+price(vehicle_type, days) tuple
}
class WeeklyRate {
+price(vehicle_type, days) tuple
}
class AddOn {
<<abstract>>
+add_on_type: AddOnType
+price(vehicle_type, days) tuple
+line(base, vehicle_type, days) InvoiceLine
}
class GpsAddOn
class ChildSeatAddOn
class InsuranceAddOn
class AddOnFactory {
+decorate(plan, add_ons) RatePlan
}
class ReturnCharges {
+lines(reservation, damage_fee) tuple
}
class InvoiceLine {
+label: str
+amount: Money
}
class Invoice {
+id: str
+lines: tuple
+total Money
}
class PaymentProcessor {
<<interface>>
+charge(amount, method) bool
}
class Payment {
+amount: Money
+method: PaymentMethod
}
class RentalSystem
RatePlan <|.. DailyRate
RatePlan <|.. WeeklyRate
RatePlan <|.. AddOn
AddOn o-- "1" RatePlan : wraps
AddOn <|-- GpsAddOn
AddOn <|-- ChildSeatAddOn
AddOn <|-- InsuranceAddOn
AddOnFactory ..> AddOn : builds
WeeklyRate o-- "1" DailyRate
ReturnCharges o-- "1" DailyRate
Invoice "1" *-- "many" InvoiceLine
RentalSystem --> RatePlan
RentalSystem --> ReturnCharges
RentalSystem --> PaymentProcessor
RentalSystem ..> Payment
Design patterns applied¶
| Pattern | Where | Why it earns its place |
|---|---|---|
| Strategy | RatePlan with DailyRate and WeeklyRate |
"Now add a weekend rate" is a new class and one constructor argument. WeeklyRate composes DailyRate instead of subclassing it, so the day rate lives in one place. |
| Decorator | AddOn and its three subclasses |
Add-ons compose in any number and any order, which a pair of booleans on Reservation cannot do. InsuranceAddOn settles the argument: it charges 20% of everything it wraps, so it must see the lines below it. |
| Object Pool | FleetPool.acquire / release |
Cars are expensive, finite and reused. The desk borrows the next acceptable car and gives it back; retirement removes it from the pool for good. |
| Factory Method | VehicleFactory.create, AddOnFactory.decorate |
Fleet imports and stored reservations carry strings. A registry dict maps them to classes; adding a pickup truck touches the registry, nothing else. |
| Facade | RentalSystem |
Six verbs over branches, pricing and payments. The API layer never touches a FleetPool. |
| State (lightweight) | ReservationStatus with guarded transitions |
An enum plus one _claim helper is enough. Full State classes would add five files and no behaviour. |
| Dependency Injection | Clock, three IdGenerators, RatePlan, PaymentProcessor |
Tests pin the date to 2026-03-01 and get R-1, INV-1, M-1 every run. |
What was deliberately not used: a Repository per entity. The brief lists it, and in production you would have ReservationRepository and VehicleRepository behind protocols — but in a 45-minute round they add indirection before they add value. Say the sentence out loud instead: "Branch and RentalSystem hold dicts today; both are one protocol away from a database, and the transaction boundary is already the branch." That is the answer the interviewer wants, and it costs you ten seconds instead of ten minutes.
Key flows¶
Booking: the branch is the only thing that may say yes.
sequenceDiagram
autonumber
actor C as Customer
participant S as RentalSystem
participant B as Branch
participant P as FleetPool
participant V as VehicleCalendar
C->>S: search(Lisbon, suv, 2026-03-05..03-12)
S->>B: available(suv, period)
B->>P: serviceable(suv)
P-->>B: three cars
B->>V: is_free(period) per car
V-->>B: one car has a workshop block
B-->>S: 2 free
S-->>C: LIS-DOWNTOWN has 2
C->>S: reserve(suv, period, add_ons)
S->>B: available(...) as a hint
S->>B: hold(reservation)
alt capacity re-checked under the branch lock
B->>B: record the hold in the ledger
B-->>S: ok
S-->>C: Reservation(RESERVED)
else another desk took the last car
B-->>S: NoVehicleAvailableError
S-->>C: rejected
end
Pickup and return: the plate appears at the desk, and the bill is assembled from three sources.
sequenceDiagram
autonumber
actor C as Customer
participant S as RentalSystem
participant B as Branch
participant P as FleetPool
participant R as RatePlan
participant X as ReturnCharges
C->>S: pick_up(reservation_id)
S->>S: claim RESERVED to PICKED_UP
S->>B: check_out(reservation)
B->>P: acquire(upgrade ladder, period, label)
P-->>B: Suv 12-SU-01, calendar blocked
B-->>S: vehicle
S-->>C: plate, odometer, fuel
Note over C,S: days pass
C->>S: return_vehicle(id, 2026-03-14, odometer, fuel, damage)
S->>S: claim PICKED_UP to RETURNED
S->>B: check_in(reservation, return_date, needs_service)
B->>P: extend the block, unblock, release
P-->>B: displaced workshop slots
S->>R: price(class, days) through the add-on decorators
S->>X: lines(reservation, damage_fee)
S-->>C: Invoice with every line
Reservation lifecycle. PICKED_UP is what makes the counting safe: the moment a reservation is pinned to a plate, it stops being a counted hold and becomes a calendar block, so it is never counted twice.
stateDiagram-v2
[*] --> RESERVED : hold accepted by the branch
RESERVED --> PICKED_UP : plate pinned at the desk
RESERVED --> CANCELLED : customer cancels
RESERVED --> NO_SHOW : pickup window elapses
PICKED_UP --> RESERVED : hand-over failed, claim reverted
PICKED_UP --> RETURNED : car back on a lot
RETURNED --> [*]
CANCELLED --> [*]
NO_SHOW --> [*]
Implementation¶
Write it in the order you would in the room: the vocabulary, then the one value object every other class depends on, then the entities, then the policies, then the services that hold the locks.
The enums pin the vocabulary. UPGRADE_LADDER next to them is deliberate — it is a business rule, not an implementation detail, and putting it in the model file stops it being reinvented inside three services:
class VehicleType(StrEnum):
"""The commercial class a customer books. Cars are booked by type, never by plate."""
ECONOMY = "economy"
SEDAN = "sedan"
SUV = "suv"
VAN = "van"
LUXURY = "luxury"
class VehicleStatus(StrEnum):
AVAILABLE = "available" # on the lot, can be handed over
RENTED = "rented" # checked out of the fleet pool
MAINTENANCE = "maintenance" # in the workshop, invisible to the desk
RETIRED = "retired" # sold or written off; never counted again
class ReservationStatus(StrEnum):
RESERVED = "reserved" # holds one counted slot of its type at the branch
PICKED_UP = "picked_up" # pinned to a plate; the slot is now a calendar block
RETURNED = "returned"
CANCELLED = "cancelled"
NO_SHOW = "no_show"
class AddOnType(StrEnum):
GPS = "gps"
CHILD_SEAT = "child_seat"
INSURANCE = "insurance"
class PaymentMethod(StrEnum):
CARD = "card"
CASH = "cash"
#: Which classes may be handed over when the booked class is out at the desk.
#: A van seats seven, so nothing substitutes for it; a luxury downgrade is a
#: refund conversation, not an upgrade, so luxury only ever gets luxury.
UPGRADE_LADDER: dict[VehicleType, tuple[VehicleType, ...]] = {
VehicleType.ECONOMY: (VehicleType.ECONOMY, VehicleType.SEDAN, VehicleType.SUV),
VehicleType.SEDAN: (VehicleType.SEDAN, VehicleType.SUV, VehicleType.LUXURY),
VehicleType.SUV: (VehicleType.SUV, VehicleType.LUXURY),
VehicleType.VAN: (VehicleType.VAN,),
VehicleType.LUXURY: (VehicleType.LUXURY,),
}
DateRange is the class the whole problem turns on. Half-open means a rental ending on the 12th does not occupy the 12th, so the next customer collects the same car that morning — and overlap is a single expression with no +1 anywhere:
@dataclass(frozen=True, slots=True, order=True)
class DateRange:
"""A **half-open** interval ``[start, end)`` over calendar days.
Half-open is the whole trick: a rental that ends on the 4th does *not*
occupy the 4th, so the next customer can pick the same car up that morning.
Two ranges overlap iff ``a.start < b.end and b.start < a.end`` -- one line,
no off-by-one, and it is the same predicate a SQL ``tstzrange && tstzrange``
exclusion constraint would apply.
"""
start: date
end: date
def __post_init__(self) -> None:
if self.end <= self.start:
raise ValidationError(f"end {self.end} must be after start {self.start}")
@property
def days(self) -> int:
return (self.end - self.start).days
def overlaps(self, other: DateRange) -> bool:
return self.start < other.end and other.start < self.end
def extended_to(self, new_end: date) -> DateRange:
return DateRange(self.start, max(new_end, self.end))
def __str__(self) -> str:
return f"{self.start.isoformat()}..{self.end.isoformat()}"
Each car owns a calendar of labelled blocks. block refuses an overlap outright, block_or_merge is for a car that comes back damaged into a slot the workshop already holds, and extend is the late-return path: the physical car really is late, so the extension always wins and the method reports what it ran into.
class VehicleCalendar:
"""The blocked date ranges of one vehicle: rentals and maintenance windows.
Deliberately *not* thread-safe. ``Branch`` owns the only lock and calls this
class under it, so a vehicle's calendar and the branch's booking ledger can
never disagree with each other.
"""
__slots__ = ("_blocks",)
def __init__(self) -> None:
self._blocks: list[tuple[str, DateRange]] = []
def blocks(self) -> list[tuple[str, DateRange]]:
return list(self._blocks)
def conflict(self, period: DateRange) -> str | None:
"""Label of the first block overlapping ``period``, or None."""
for label, blocked in self._blocks:
if blocked.overlaps(period):
return label
return None
def is_free(self, period: DateRange) -> bool:
return self.conflict(period) is None
def block(self, label: str, period: DateRange) -> None:
clash = self.conflict(period)
if clash is not None:
raise OverlappingReservationError(f"{period} overlaps existing block {clash}")
self._blocks.append((label, period))
def block_or_merge(self, label: str, period: DateRange) -> str:
"""Block ``period``, or grow the block it clashes with so it covers ``period``.
A car that comes back damaged inside a window the workshop already holds
needs one continuous slot, not two overlapping ones. Returns the label of
the block that ended up covering the range.
"""
clash = self.conflict(period)
if clash is None:
self._blocks.append((label, period))
return label
for index, (name, blocked) in enumerate(self._blocks):
if name == clash:
merged = DateRange(min(blocked.start, period.start), max(blocked.end, period.end))
self._blocks[index] = (name, merged)
return name
return label
def unblock(self, label: str) -> None:
self._blocks = [entry for entry in self._blocks if entry[0] != label]
def extend(self, label: str, new_end: date) -> list[str]:
"""Grow a block because the car came back late.
Returns the labels the grown block now runs into -- the late-return
cascade. The physical car really is late, so the extension always wins;
the caller decides what to do with the bookings it displaced.
"""
for index, (name, period) in enumerate(self._blocks):
if name != label:
continue
grown = period.extended_to(new_end)
self._blocks[index] = (name, grown)
return [other for other, blk in self._blocks if other != label and blk.overlaps(grown)]
raise NotFoundError(f"no calendar block labelled {label}")
Vehicles carry their own class and capacity, so no service ever branches on a type string:
class Vehicle(ABC):
"""One physical car. Subclasses fix the commercial class and its capacity.
The desk never asks "what type are you?" in an if/elif ladder: it asks the
fleet pool for a class, and the class comes from the object itself.
"""
def __init__(
self, plate: str, branch_id: str, odometer_km: int = 0, fuel_eighths: int = 8
) -> None:
if not plate or not plate.strip():
raise ValidationError("plate must be non-empty")
if not 0 <= fuel_eighths <= 8:
raise ValidationError("fuel is measured in eighths of a tank (0-8)")
self.plate = plate.strip().upper()
self.branch_id = branch_id
self.odometer_km = odometer_km
self.fuel_eighths = fuel_eighths
self.status = VehicleStatus.AVAILABLE
self.calendar = VehicleCalendar()
@property
@abstractmethod
def vehicle_type(self) -> VehicleType: ...
@property
@abstractmethod
def seats(self) -> int: ...
def __repr__(self) -> str:
return f"{type(self).__name__}({self.plate!r})"
class EconomyCar(Vehicle):
vehicle_type = VehicleType.ECONOMY
seats = 4
class Sedan(Vehicle):
vehicle_type = VehicleType.SEDAN
seats = 5
class Suv(Vehicle):
vehicle_type = VehicleType.SUV
seats = 5
class Van(Vehicle):
vehicle_type = VehicleType.VAN
seats = 7
class LuxuryCar(Vehicle):
vehicle_type = VehicleType.LUXURY
seats = 4
class VehicleFactory:
"""Factory Method: fleet import files carry a class string, not a Python type."""
_registry: dict[VehicleType, type[Vehicle]] = {
VehicleType.ECONOMY: EconomyCar,
VehicleType.SEDAN: Sedan,
VehicleType.SUV: Suv,
VehicleType.VAN: Van,
VehicleType.LUXURY: LuxuryCar,
}
@classmethod
def create(cls, vehicle_type: VehicleType | str, plate: str, branch_id: str, **kwargs: int) -> Vehicle:
try:
klass = cls._registry[VehicleType(vehicle_type)]
except ValueError as exc:
raise ValidationError(f"unknown vehicle type: {vehicle_type!r}") from exc
return klass(plate, branch_id, **kwargs)
Rate plans return lines, not a total. That one choice makes the itemised invoice free and lets insurance price itself off the lines above it:
DEFAULT_DAILY_RATES: dict[VehicleType, Money] = {
VehicleType.ECONOMY: Money.of("35.00"),
VehicleType.SEDAN: Money.of("45.00"),
VehicleType.SUV: Money.of("65.00"),
VehicleType.VAN: Money.of("80.00"),
VehicleType.LUXURY: Money.of("120.00"),
}
class RatePlan(Protocol):
"""Turns a class and a number of days into itemised invoice lines.
Lines rather than a single total: the customer sees why they pay, and the
insurance add-on can price itself as a percentage of everything above it.
"""
def price(self, vehicle_type: VehicleType, days: int) -> tuple[InvoiceLine, ...]: ...
class DailyRate:
"""One rate per class per day. The baseline every other plan composes."""
def __init__(self, rates: dict[VehicleType, Money] | None = None) -> None:
self._rates = rates or DEFAULT_DAILY_RATES
def rate_for(self, vehicle_type: VehicleType) -> Money:
return self._rates[vehicle_type]
def price(self, vehicle_type: VehicleType, days: int) -> tuple[InvoiceLine, ...]:
if days <= 0:
raise ValidationError("a rental is at least one day")
rate = self.rate_for(vehicle_type)
label = f"{vehicle_type} rental: {days} day(s) x {rate}"
return (InvoiceLine(label, rate * days),)
class WeeklyRate:
"""Seven days cost six. Composes ``DailyRate`` instead of subclassing it."""
def __init__(self, daily: DailyRate, charged_days_per_week: int = 6) -> None:
self._daily = daily
self._charged = charged_days_per_week
def price(self, vehicle_type: VehicleType, days: int) -> tuple[InvoiceLine, ...]:
if days <= 0:
raise ValidationError("a rental is at least one day")
rate = self._daily.rate_for(vehicle_type)
weeks, extra = divmod(days, 7)
if not weeks:
return (InvoiceLine(f"{vehicle_type} rental: {days} day(s) x {rate}", rate * days),)
label = f"{vehicle_type} rental: {weeks} week(s) x {self._charged} day(s) x {rate}"
lines = [InvoiceLine(label, rate * (weeks * self._charged))]
if extra:
lines.append(InvoiceLine(f"{extra} extra day(s) x {rate}", rate * extra))
return tuple(lines)
class AddOn(ABC):
"""Decorator: wraps any ``RatePlan`` and appends one line to its output.
Add-ons compose in any order and in any number, which an ``add_gps: bool``
flag on the reservation cannot do -- and ``InsuranceAddOn`` proves the point,
because it prices itself off whatever it wraps.
"""
add_on_type: AddOnType
def __init__(self, inner: RatePlan) -> None:
self._inner = inner
def price(self, vehicle_type: VehicleType, days: int) -> tuple[InvoiceLine, ...]:
base = self._inner.price(vehicle_type, days)
return (*base, self.line(base, vehicle_type, days))
@abstractmethod
def line(
self, base: tuple[InvoiceLine, ...], vehicle_type: VehicleType, days: int
) -> InvoiceLine: ...
class GpsAddOn(AddOn):
add_on_type = AddOnType.GPS
RATE = Money.of("4.00")
def line(self, base: tuple[InvoiceLine, ...], vehicle_type: VehicleType, days: int) -> InvoiceLine:
return InvoiceLine(f"GPS unit: {days} day(s) x {self.RATE}", self.RATE * days)
class ChildSeatAddOn(AddOn):
add_on_type = AddOnType.CHILD_SEAT
RATE = Money.of("6.50")
def line(self, base: tuple[InvoiceLine, ...], vehicle_type: VehicleType, days: int) -> InvoiceLine:
return InvoiceLine(f"child seat: {days} day(s) x {self.RATE}", self.RATE * days)
class InsuranceAddOn(AddOn):
"""Collision damage waiver: a percentage of everything wrapped so far."""
add_on_type = AddOnType.INSURANCE
PERCENT = Decimal("0.20")
def line(self, base: tuple[InvoiceLine, ...], vehicle_type: VehicleType, days: int) -> InvoiceLine:
subtotal = total_of(base)
return InvoiceLine("damage waiver: 20% of the lines above", subtotal * self.PERCENT)
class AddOnFactory:
"""Maps the codes stored on a reservation back to decorator classes."""
_registry: dict[AddOnType, type[AddOn]] = {
AddOnType.GPS: GpsAddOn,
AddOnType.CHILD_SEAT: ChildSeatAddOn,
AddOnType.INSURANCE: InsuranceAddOn,
}
@classmethod
def decorate(cls, plan: RatePlan, add_ons: Iterable[AddOnType | str]) -> RatePlan:
"""Wrap ``plan`` once per add-on. Insurance is applied last on purpose."""
try:
codes = [AddOnType(a) for a in add_ons]
except ValueError as exc:
raise ValidationError(f"unknown add-on: {exc}") from exc
for add_on in sorted(codes, key=lambda a: a is AddOnType.INSURANCE):
plan = cls._registry[add_on](plan)
return plan
The pool only selects and mutates; it takes no lock, because Branch serialises every call into it. That separation is what keeps a car's calendar and the branch ledger consistent:
class FleetPool:
"""Object Pool over one branch's cars: checked out at pickup, checked in at return.
Cars are expensive and finite, so the desk never *creates* one -- it borrows
the next serviceable car of an acceptable class and gives it back. The pool
takes no lock of its own: ``Branch`` serialises every call (see below), which
is what keeps the calendars and the booking ledger consistent with each other.
"""
def __init__(self, vehicles: Iterable[Vehicle] = ()) -> None:
self._by_plate: dict[str, Vehicle] = {v.plate: v for v in vehicles}
def add(self, vehicle: Vehicle) -> None:
self._by_plate[vehicle.plate] = vehicle
def remove(self, plate: str) -> Vehicle:
try:
return self._by_plate.pop(plate)
except KeyError:
raise UnknownVehicleError(f"no vehicle {plate}") from None
def get(self, plate: str) -> Vehicle:
try:
return self._by_plate[plate]
except KeyError:
raise UnknownVehicleError(f"no vehicle {plate}") from None
def serviceable(self, vehicle_type: VehicleType) -> list[Vehicle]:
"""Cars of that class still on the books (a retired car is gone forever)."""
return [
v
for v in self._by_plate.values()
if v.vehicle_type is vehicle_type and v.status is not VehicleStatus.RETIRED
]
def acquire(self, classes: Sequence[VehicleType], period: DateRange, label: str) -> Vehicle:
"""Borrow the first free car, walking the upgrade ladder if the booked class is out."""
for vehicle_type in classes:
for vehicle in self.serviceable(vehicle_type):
if vehicle.status is VehicleStatus.AVAILABLE and vehicle.calendar.is_free(period):
vehicle.calendar.block(label, period)
vehicle.status = VehicleStatus.RENTED
return vehicle
raise NoVehicleAvailableError(f"no car of {list(classes)} free for {period}")
def release(self, plate: str, needs_service: bool, today: date) -> Vehicle:
"""Give the car back. Damage sends it to the workshop and blocks its calendar."""
vehicle = self.get(plate)
if not needs_service:
vehicle.status = VehicleStatus.AVAILABLE
return vehicle
vehicle.status = VehicleStatus.MAINTENANCE
window = DateRange(today, today + timedelta(days=SERVICE_DAYS_AFTER_DAMAGE))
vehicle.calendar.block_or_merge(f"service:{plate}", window)
return vehicle
Branch is the unit of locking and the only object allowed to say "yes, you may have a car". _available is the whole oversell argument in five lines:
class Branch:
"""One rental location: its fleet, its booking ledger, and *the* lock over both.
``Branch._lock`` is the serialisation point of the whole design. It is coarser
than one lock per car (because "is anything of this class free?" would then be
a multi-lock scan) and far finer than one lock for the company (two cities
never contend). Everything it guards has to agree at all times: the counted
availability for a class, the ledger of holds, and every vehicle calendar.
"""
def __init__(self, branch_id: str, city: str, vehicles: Iterable[Vehicle] = ()) -> None:
self.id = branch_id
self.city = city
self._pool = FleetPool(vehicles)
self._bookings: dict[str, Reservation] = {}
self.lock = threading.RLock()
def fleet_size(self, vehicle_type: VehicleType) -> int:
with self.lock:
return len(self._pool.serviceable(vehicle_type))
def available(self, vehicle_type: VehicleType, period: DateRange) -> int:
with self.lock:
return self._available(vehicle_type, period)
def _available(self, vehicle_type: VehicleType, period: DateRange) -> int:
"""Cars of the class, minus those with a clashing calendar block, minus open holds.
Membership of ``_bookings`` *is* the hold: a row is inserted by ``hold``
and removed only by ``check_out`` (which pins a plate in the same locked
block) or by ``release_hold``. Do not additionally filter on
``status is RESERVED`` -- ``pick_up`` flips the status under the registry
lock before ``check_out`` takes the branch lock, so for that window the
reservation would be counted in neither term and the class would look one
car freer than it is. Counting the ledger closes the window, and nothing
is ever counted twice because the row is gone before the block exists.
"""
fleet = self._pool.serviceable(vehicle_type)
blocked = sum(1 for v in fleet if not v.calendar.is_free(period))
held = sum(
1
for r in self._bookings.values()
if r.vehicle_type is vehicle_type and r.period.overlaps(period)
)
return len(fleet) - blocked - held
def hold(self, reservation: Reservation) -> None:
"""Atomically re-check capacity and record the hold. The oversell guard."""
with self.lock:
if self._available(reservation.vehicle_type, reservation.period) <= 0:
raise NoVehicleAvailableError(
f"no {reservation.vehicle_type} free at {self.id} for {reservation.period}"
)
self._bookings[reservation.id] = reservation
def release_hold(self, reservation_id: str) -> None:
with self.lock:
self._bookings.pop(reservation_id, None)
def check_out(self, reservation: Reservation) -> Vehicle:
"""Pin a plate to the reservation, upgrading if the booked class is out."""
with self.lock:
ladder = UPGRADE_LADDER[reservation.vehicle_type]
vehicle = self._pool.acquire(ladder, reservation.period, reservation.id)
self._bookings.pop(reservation.id, None)
return vehicle
def check_in(
self, reservation: Reservation, return_date: date, needs_service: bool
) -> tuple[Vehicle, list[str]]:
"""Close the calendar block (extending it first if the car is late) and free the car."""
with self.lock:
plate = reservation.plate or ""
vehicle = self._pool.get(plate)
displaced = vehicle.calendar.extend(reservation.id, return_date)
vehicle.calendar.unblock(reservation.id)
if reservation.return_odometer is not None:
vehicle.odometer_km = reservation.return_odometer
if reservation.return_fuel is not None:
vehicle.fuel_eighths = reservation.return_fuel
self._pool.release(plate, needs_service, return_date)
return vehicle, displaced
def schedule_maintenance(self, record: MaintenanceRecord) -> MaintenanceRecord:
"""Block a car's calendar for a service window; overlaps are rejected outright."""
with self.lock:
vehicle = self._pool.get(record.plate)
vehicle.calendar.block(f"maint:{record.id}", record.period)
return record
def adopt(self, vehicle: Vehicle) -> None:
with self.lock:
vehicle.branch_id = self.id
self._pool.add(vehicle)
def surrender(self, plate: str) -> Vehicle:
with self.lock:
return self._pool.remove(plate)
def transfer(source: Branch, target: Branch, plate: str) -> Vehicle:
"""Move a car between branches after a one-way rental.
Both locks are needed, so they are taken in branch-id order: two one-way
returns running in opposite directions can then never deadlock.
"""
first, second = sorted((source, target), key=lambda b: b.id)
with first.lock, second.lock:
vehicle = source.surrender(plate)
target.adopt(vehicle)
return vehicle
The facade never holds its own lock across a branch call. Every status change goes through _claim, which is a check-and-flip under the registry lock — the same "reserve, do, commit or revert" shape the parking lot uses at its exit gate:
class RentalSystem:
"""Facade: search, reserve, cancel, pick up, return, pay -- one object per company.
Two locks, never nested: this one guards the reservation registry and every
status transition; ``Branch.lock`` guards fleet and capacity. A transition is
*claimed* under this lock, the branch work happens outside it, and the claim
is reverted if that work fails -- the same reserve/do/commit shape the parking
lot uses at its exit gate.
"""
FREE_CANCELLATION_DAYS = 1
def __init__(
self,
branches: Iterable[Branch],
rate_plan: RatePlan | None = None,
charges: ReturnCharges | None = None,
processor: PaymentProcessor | None = None,
clock: Clock | None = None,
ids: IdGenerator | None = None,
invoice_ids: IdGenerator | None = None,
maintenance_ids: IdGenerator | None = None,
) -> None:
daily = DailyRate()
self._branches = {b.id: b for b in branches}
self._rate_plan = rate_plan or WeeklyRate(daily)
self._charges = charges or ReturnCharges(daily)
self._daily = daily
self._processor = processor or AlwaysApprovesProcessor()
self._clock = clock or SystemClock()
self._ids = ids or SequentialIdGenerator("R")
self._invoice_ids = invoice_ids or SequentialIdGenerator("INV")
self._maintenance_ids = maintenance_ids or SequentialIdGenerator("M")
self._reservations: dict[str, Reservation] = {}
self._lock = threading.Lock()
def today(self) -> date:
return self._clock.now_dt().date()
def branch(self, branch_id: str) -> Branch:
try:
return self._branches[branch_id]
except KeyError:
raise UnknownBranchError(f"no branch {branch_id}") from None
def reservation(self, reservation_id: str) -> Reservation:
with self._lock:
try:
return self._reservations[reservation_id]
except KeyError:
raise UnknownReservationError(f"unknown reservation {reservation_id}") from None
def search(self, city: str, vehicle_type: VehicleType, period: DateRange) -> list[tuple[str, int]]:
"""Branches in the city with at least one car of the class free for the whole range."""
found = [
(b.id, b.available(vehicle_type, period))
for b in self._branches.values()
if b.city.casefold() == city.casefold()
]
return sorted([(bid, n) for bid, n in found if n > 0], key=lambda row: (-row[1], row[0]))
def quote(self, vehicle_type: VehicleType, period: DateRange, add_ons: Iterable[AddOnType] = ()) -> Invoice:
"""Price a rental before it exists -- the same code path the invoice uses."""
plan = AddOnFactory.decorate(self._rate_plan, add_ons)
return Invoice(id="quote", reservation_id="-", lines=plan.price(vehicle_type, period.days))
def reserve(
self,
customer_id: str,
vehicle_type: VehicleType,
pickup_branch: str,
period: DateRange,
dropoff_branch: str | None = None,
add_ons: Iterable[AddOnType] = (),
) -> Reservation:
if period.start < self.today():
raise ValidationError(f"cannot book {period}: it starts in the past")
drop = dropoff_branch or pickup_branch
branch, target = self.branch(pickup_branch), self.branch(drop)
if branch.available(VehicleType(vehicle_type), period) <= 0: # cheap hint; hold() is the truth
raise NoVehicleAvailableError(f"no {vehicle_type} free at {branch.id} for {period}")
reservation = Reservation(
id=self._ids.next_id(),
customer_id=customer_id,
vehicle_type=VehicleType(vehicle_type),
pickup_branch=branch.id,
dropoff_branch=target.id,
period=period,
add_ons=tuple(AddOnType(a) for a in add_ons),
)
branch.hold(reservation) # raises NoVehicleAvailableError; nothing recorded yet
with self._lock:
self._reservations[reservation.id] = reservation
return reservation
def cancel(self, reservation_id: str) -> Money:
"""Free the counted slot. Cancelling inside the free window costs one day."""
reservation = self._claim(reservation_id, ReservationStatus.RESERVED, ReservationStatus.CANCELLED)
self.branch(reservation.pickup_branch).release_hold(reservation.id)
days_ahead = (reservation.period.start - self.today()).days
if days_ahead >= self.FREE_CANCELLATION_DAYS:
return Money(0)
return self._daily.rate_for(reservation.vehicle_type)
def mark_no_show(self, reservation_id: str) -> Reservation:
reservation = self._claim(reservation_id, ReservationStatus.RESERVED, ReservationStatus.NO_SHOW)
self.branch(reservation.pickup_branch).release_hold(reservation.id)
return reservation
def pick_up(self, reservation_id: str) -> Vehicle:
"""RESERVED -> PICKED_UP, then pin a plate. A failed hand-over rolls the status back."""
reservation = self._claim(reservation_id, ReservationStatus.RESERVED, ReservationStatus.PICKED_UP)
try:
vehicle = self.branch(reservation.pickup_branch).check_out(reservation)
except NoVehicleAvailableError:
self._revert(reservation, ReservationStatus.RESERVED)
raise
reservation.plate = vehicle.plate
reservation.handed_over_type = vehicle.vehicle_type
reservation.pickup_odometer = vehicle.odometer_km
reservation.pickup_fuel = vehicle.fuel_eighths
return vehicle
def return_vehicle(
self,
reservation_id: str,
return_date: date,
odometer_km: int,
fuel_eighths: int,
damage_fee: Money | None = None,
) -> tuple[Invoice, list[str]]:
"""PICKED_UP -> RETURNED. Returns the invoice and any service slots the car ate into."""
reservation = self._claim(reservation_id, ReservationStatus.PICKED_UP, ReservationStatus.RETURNED)
if return_date < reservation.period.start:
self._revert(reservation, ReservationStatus.PICKED_UP)
raise ValidationError(f"return date {return_date} precedes pickup {reservation.period.start}")
reservation.return_date = return_date
reservation.return_odometer = odometer_km
reservation.return_fuel = fuel_eighths
pickup = self.branch(reservation.pickup_branch)
damaged = damage_fee is not None and not damage_fee.is_zero()
_, displaced = pickup.check_in(reservation, return_date, needs_service=damaged)
if reservation.is_one_way:
transfer(pickup, self.branch(reservation.dropoff_branch), reservation.plate or "")
plan = AddOnFactory.decorate(self._rate_plan, reservation.add_ons)
lines: list[InvoiceLine] = list(plan.price(reservation.vehicle_type, reservation.period.days))
lines.extend(self._charges.lines(reservation, damage_fee))
return Invoice(self._invoice_ids.next_id(), reservation.id, tuple(lines)), displaced
def schedule_maintenance(self, branch_id: str, plate: str, period: DateRange, reason: str) -> MaintenanceRecord:
record = MaintenanceRecord(self._maintenance_ids.next_id(), plate.upper(), period, reason)
return self.branch(branch_id).schedule_maintenance(record)
def pay(self, invoice: Invoice, method: PaymentMethod) -> Payment:
if not self._processor.charge(invoice.total, method):
raise ReservationStateError(f"{method} payment of {invoice.total} declined")
return Payment(f"PAY-{invoice.id}", invoice.id, invoice.total, method, self._clock.now())
def _claim(self, reservation_id: str, expected: ReservationStatus, target: ReservationStatus) -> Reservation:
"""Check-and-flip the status atomically so two desks cannot both act on it."""
with self._lock:
reservation = self._reservations.get(reservation_id)
if reservation is None:
raise UnknownReservationError(f"unknown reservation {reservation_id}")
if reservation.status is not expected:
raise ReservationStateError(
f"reservation {reservation_id} is {reservation.status}, expected {expected}"
)
reservation.status = target
return reservation
def _revert(self, reservation: Reservation, status: ReservationStatus) -> None:
with self._lock:
reservation.status = status
Running python -m lld.car_rental.demo walks a whole week, including the late return that eats its own workshop slot:
today is 2026-03-01; searching suv in Lisbon for 2026-03-05..2026-03-12
LIS-DOWNTOWN: 2 suv free
workshop booked for both suvs; availability for the week is now 1
R-1 reserved: suv 2026-03-05..2026-03-12 with gps, insurance -> quote 501.60 USD
second suv request rejected: no suv free at LIS-DOWNTOWN for 2026-03-05..2026-03-12
R-2 reserved: economy 2026-03-05..2026-03-08, one-way to LIS-AIRPORT
R-2 picked up 10-EC-01; R-1 picked up 12-SU-01 at 41200 km, fuel 8/8
R-2 dropped at LIS-AIRPORT: INV-1 totals 243.00 USD (one-way fee included)
R-1 back on 2026-03-14, two days late and damaged; it displaced ['maint:M-2']
suv rental: 1 week(s) x 6 day(s) x 65.00 USD: 390.00 USD
GPS unit: 7 day(s) x 4.00 USD: 28.00 USD
damage waiver: 20% of the lines above: 83.60 USD
late return: 2 day(s) x 97.50 USD: 195.00 USD
refuelling: 3/8 tank: 27.00 USD
mileage: 900 km past the 1800 km allowance: 225.00 USD
damage assessment: 150.00 USD
PAY-INV-2: 1098.60 USD by card; 12-SU-01 goes straight to the workshop
Concurrency and edge cases¶
Which lock protects what. There are exactly two kinds, and the two kinds are never nested with each other:
Branch.lock(anRLock) guards that branch's fleet pool, every vehicle calendar in it, and the ledger of open holds. It is the serialisation point of the design. Coarser than one lock per car — because "is any SUV free that week?" would otherwise be a scan that takes N locks in an order you have to prove is safe — and far finer than one lock for the company, so Lisbon and Porto never contend. An uncontended lock costs about 17 ns; a branch that handles even 100 bookings a minute is nowhere near contention, so buying simplicity with a slightly coarse lock is the right trade.RentalSystem._lockguards the reservation registry and every status transition._claim(id, expected, target)checks and flips atomically, so two desks handed the same reservation cannot both pick the car up: the loser getsReservationStateError.
The ordering rule is stated once and obeyed everywhere: claim under the registry lock, then do the branch work outside it, then revert the claim if that work failed. pick_up is the example — if check_out cannot find a car, the reservation goes back to RESERVED and the customer keeps their booking.
Preventing oversell. Branch._available is the invariant:
fleet = cars of that class that are not retired
blocked = those whose calendar overlaps the range # maintenance + already-pinned rentals
held = open ledger rows for that class overlapping the range
available = len(fleet) - blocked - held
The subtlety worth saying out loud: a reservation is counted in exactly one of the two terms — never both, and never neither. Ledger membership is the hold, not the status: pick_up flips the status before check_out pins a plate, so counting by status frees the car for that window. reserve does a cheap unlocked available() call first as a hint, then calls hold, which recomputes under the lock and is the only thing that can say yes.
Two branch locks at once. A one-way return has to remove a car from one fleet and add it to another. transfer sorts the two branches by id and takes the locks in that order, so two one-way returns running in opposite directions can never deadlock — the classic fixed-order rule, applied to the only place in this design that needs two locks.
The late-return cascade. A car that comes back two days late has already broken somebody's plan. VehicleCalendar.extend grows the rental block and returns the labels it now overlaps, so the branch learns immediately that its workshop slot is gone. Nothing downstream breaks, and that is the payoff of booking by class: the next customer is not waiting for that plate, they are waiting for an SUV, so the desk can hand them a different one — or walk the upgrade ladder.
Edge cases handled: back-to-back rentals on one plate (half-open ranges), zero-length and reversed date ranges rejected at construction, booking a date in the past, cancelling twice, cancelling after pickup, an overlapping maintenance window, a damaged car returning into a workshop slot it already had, a one-way return into a branch with no cars of that class, mileage and fuel deltas, and a return date before the pickup date.
Common mistake
Modelling the reservation as a link to a specific plate at booking time. It feels tidier and it fails the moment a car is late, breaks down or is written off: every downstream booking on that plate has to be rewritten, under a lock, with a fallback nobody designed. Book the class, count the fleet, pin the plate at the desk — and if the interviewer pushes back, point at the one-line _available formula and the upgrade ladder.
Extensibility and follow-ups¶
- Dynamic pricing: a
DemandRate(daily, utilisation_provider)that reads branch utilisation and multiplies. It implementsRatePlan, so it slots in as a constructor argument and every add-on still composes on top. - Loyalty: a
LoyaltyDiscount(RatePlan)decorator that appends a negative line. Decorators already compose, so gold-tier plus insurance plus GPS needs no new plumbing — only a decision about ordering, which is exactly the discussion you want to be having. - Fleet rebalancing:
transferalready exists and is deadlock-safe. A rebalancer is a job that reads utilisation per branch per class and issues transfers; the only new rule is that a car with a future block cannot leave. - Damage photos:
MaintenanceRecordgrows anevidence: tuple[str, ...]of object-storage keys. The domain does not change; the upload path is an HLD conversation about presigned URLs. - Hourly rentals and car sharing: swap
datefordatetimeinsideDateRange. Every overlap check is already written against the interval, not against days. - Persistence and multi-region:
Branchis the natural transaction boundary — one row per hold, and the per-vehicle blocks become an exclusion constraint on a range column, which is the same predicateDateRange.overlapsimplements. That mapping is what makes this design portable rather than clever.
Interview tip
When you draw the timeline, draw it half-open and say so: "the 12th belongs to the next rental". Then write a.start < b.end and b.start < a.end on the board and note it is the same predicate a database range-exclusion constraint uses. Two sentences, and you have shown you know both the domain rule and how it survives contact with a real database.
Tests¶
tests/test_car_rental.py has 21 cases. Two are worth walking through out loud. The concurrency test races 24 threads at a branch with three SUVs and asserts exactly three winners — the invariant, not the mechanism:
def test_concurrent_reservations_never_oversell_a_branch(clock: FakeClock) -> None:
system = make_system(clock, make_branch("LIS", "Lisbon", {VehicleType.SUV: 3}))
def book(i: int) -> str | None:
try:
return system.reserve(f"c{i}", VehicleType.SUV, "LIS", WEEK).id
except NoVehicleAvailableError:
return None
with ThreadPoolExecutor(max_workers=12) as pool:
results = list(pool.map(book, range(24)))
winners = [r for r in results if r is not None]
assert len(winners) == 3 and len(set(winners)) == 3 # three cars, three winners, no oversell
assert system.branch("LIS").available(VehicleType.SUV, WEEK) == 0
The upgrade test is the one that proves booking-by-class was the right model: the reserved car breaks down after the booking is taken, the desk hands over a sedan, and the customer is still billed the economy week rate.
def test_pickup_walks_the_upgrade_ladder_when_the_booked_class_is_out(clock: FakeClock) -> None:
branch = make_branch("LIS", "Lisbon", {VehicleType.ECONOMY: 1, VehicleType.SEDAN: 1})
system = make_system(clock, branch)
booking = system.reserve("c1", VehicleType.ECONOMY, "LIS", WEEK)
# The economy car breaks down after the booking is taken; the desk still owes a car.
system.schedule_maintenance("LIS", "LIS-EC1", DateRange(date(2026, 3, 4), date(2026, 3, 20)), "gearbox")
clock.advance(4 * 86_400)
handed_over = system.pick_up(booking.id)
assert handed_over.vehicle_type is VehicleType.SEDAN # upgraded at the desk...
invoice, _ = system.return_vehicle(booking.id, date(2026, 3, 12), odometer_km=100, fuel_eighths=8)
assert invoice.total == Money.of("210.00") # ...but billed at the economy week rate, 6 x 35.00
The rest cover: the full reserve-pickup-return-pay path; half-open overlap through a five-case parametrize (touching at either end must not overlap); back-to-back rentals landing on the same plate; rejection when every car of the class is committed, and acceptance for a neighbouring window; past dates and zero-day ranges; free versus chargeable cancellation and a second cancel failing; maintenance hiding a car and rejecting an overlapping window; the one-way transfer moving the car between fleets; the late return charging 1.5x per late day and reporting the displaced workshop slot; and the rate plans and add-on decorators through a five-case parametrize. Run them with uv run pytest code/lld/car_rental -q.
45-minute pacing¶
| Minutes | What to do | What to say or write |
|---|---|---|
| 0–5 | Clarify | By class or by plate? Days or hours? One-way allowed? What happens when the booked class is out? Out of scope: loyalty, dynamic pricing, gateway. |
| 5–10 | Entities and the timeline | Draw a calendar strip. Write DateRange half-open and the overlap predicate. Nouns: Branch, Vehicle (+5), Calendar, Reservation, Invoice. |
| 10–18 | Class diagram | Structure first, then hang RatePlan and the AddOn decorators off RentalSystem. Mark the branch lock on the diagram. |
| 18–34 | Code | DateRange.overlaps → VehicleCalendar → Branch._available → Branch.hold → RentalSystem.pick_up. Say "count the class, pin the plate" while writing it. |
| 34–40 | Concurrency and pricing | The two locks, the claim-do-revert rule, the fixed lock order for transfers. Then the insurance decorator as the reason add-ons are not booleans. |
| 40–45 | Edge cases and extensions | Late-return cascade, upgrade ladder, dynamic pricing as a new RatePlan, and the range-exclusion constraint you would use in Postgres. |
Related¶
- Design a hotel management system — the same date-range inventory problem with rooms instead of cars
- Design Ticketmaster (with a hotel-booking variant) — what this becomes at scale, with holds, TTLs and a waiting room
- Decorator — the add-on stack and why insurance must wrap
- Object Pool — acquire and release over a finite fleet
- Strategy — the rate plans
- Concurrency for LLD in Python — lock granularity and fixed lock ordering
- Primary source: PostgreSQL documentation, "Range Types" and
EXCLUDE USING gistconstraints — the database expression ofDateRange.overlaps