Skip to content

Low Level DesignWorked problems

Design a car rental system

Design a car rental system step by step for a low level design interview. Grow from renting a specific car to reservations by category and branch checked day by day, a reservation lifecycle where the car is chosen at pickup, one-way and late returns, pricing as a list of rules that produce a bill, and booking safely while many customers pay at once, with code in Java, Python, C#, Go and TypeScript.

Mid-level27 min core · 44 min full depth

“Design a car rental system like Hertz or Zoomcar.” Then: “what happens when two people book the last SUV at the same time?”

A car rental company owns cars at several branches, like an airport branch and a city branch. Customers book a kind of car, such as “an SUV”, at a branch for some days, often weeks ahead. They pick it up on the first day and bring it back at the end.

It looks like a parking lot with dates, and the dates are where it gets interesting. The system has to promise a car without knowing which one. Whether a booking fits depends on every other booking, on every day it covers. Cars come back late, or to a different branch, and the next customer still expects a car. The price is a stack of rules that marketing keeps adding to. And bookings arrive from many places at once.

If you only have a minute, this is the design. The rest of the page explains how we get here.

  1. A booking is for a category of car at a branch, for a range of days, not for one car. The actual car is chosen at pickup, from whatever is on the lot.
  2. A booking fits if, on every day of its range, fewer cars are booked than the branch has. Counting every booking that overlaps the range is wrong: two bookings on different days can share one car.
  3. A reservation moves through states: reserved, active, returned, or cancelled. Each operation checks the state first.
  4. The price is a list of rules, each adding lines to a bill. The same rules give the quote before the trip and the invoice after it.
  5. Checking and recording a booking happen as one step, under a lock. Charging the card is slow, so it happens after, and a declined card cancels the booking.

We’ll build this in four stages, then handle many customers booking at once.

Before drawing anything, we ask a few questions. Each answer changes a class. Anything that doesn’t, we assume and move on.

Interviewerminute 0

Design a car rental system.

What they are testing

Whether you find what makes this different from a parking lot: dates in the future, a kind of car instead of a car, and many branches. Jumping straight to classes for Car and Customer without asking how bookings work is the red flag.

Here are the questions that matter:

  • Do customers book ahead, or walk in? They book ahead, by whole days.
  • Do they book a specific car, or a kind of car? A category, like economy, SUV or van.
  • One location, or many? Many branches.
  • Can a car be returned at another branch? Yes, for a fee. This is a one-way rental.
  • How is the price made up? A daily rate, plus discounts and fees.

These answers give us one promise that matters more than the others: a branch never promises more cars of a category than it has, on any day. It’s the hard one, because it depends on every booking that touches each day. Two smaller promises come with it. A booking holds a car from the moment it’s recorded until it’s cancelled or the car comes back. And the bill comes from the same rules as the quote, so the two never disagree about the same trip.

We leave out hourly rentals, damage and repairs, and overbooking on purpose.

Go deeperThree more questions, and what each changes1 min
Question We assume If the answer were different
Hourly rentals? Whole days Hours instead of days; the day-by-day check becomes per slot
Maintenance and damage? Out of scope A car can be out of service, removed from the fleet count
Overbooking on purpose? No Airlines do it; a car rental could allow a few percent over

We’ll build the design in four stages. Each stage adds only the classes that a new requirement needs. In each diagram, the new parts are outlined, and the colors show what each class does (the key is under every diagram). Days are plain numbers, counted from some first day, to keep date arithmetic out of the way. A real system uses dates.

Interviewerminute 3

Start with the simplest thing: a customer walks in and rents a car.

What they are testing

Whether you can build it in two minutes, and then say exactly why it’s not a rental business. Each problem you name sets up the next stage.

Start with the smallest thing that works. A Car records who is renting it, if anyone. The RentalService rents a car by its id if it’s free, and takes it back.

Stage 1: renting a car on the spot

RentalService

-cars: Map<String, Car>

+rent(carId, customer) : bool

+giveBack(carId) : bool

Car

+id

+rentedBy

RentalService

-cars: Map<String, Car>

+rent(carId, customer) : bool

+giveBack(carId) : bool

Car

+id

+rentedBy

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation
  • New in this step
RentalService.javarental_service.pyRentalService.csrental_service.gorental-service.ts
class Car {
final String id;
String rentedBy; // null while the car is on the lot
Car(String id) { this.id = id; }
}
class RentalService {
private final Map<String, Car> cars = new LinkedHashMap<>();
void addCar(String id) { cars.put(id, new Car(id)); }
// flaw 1: a customer can only take a car now. Nobody can book one for next week.
// flaw 2: customers ask for a kind of car, not car KA-01-1234, and there is one lot.
// flaw 3: nothing says what the rental costs.
boolean rent(String carId, String customer) {
Car car = cars.get(carId);
if (car == null || car.rentedBy != null) return false;
car.rentedBy = customer;
return true;
}
boolean giveBack(String carId) {
Car car = cars.get(carId);
if (car == null || car.rentedBy == null) return false;
car.rentedBy = null;
return true;
}
}

It works, but it has three problems, and we leave them in on purpose. Nobody can book a car for next week. Customers ask for “an SUV”, not a licence plate, and there’s only one lot. And nothing says what anything costs.

Stage 2: Book a category at a branch, for some days

Section titled “Stage 2: Book a category at a branch, for some days”
Interviewerminute 5

Customers book ahead: an SUV at the airport branch, days 3 to 7. How do you decide whether one is free?

What they are testing

Whether you check availability day by day. The red flag is “count the bookings that overlap the range and compare with the fleet”: it refuses bookings that fit.

A DateRange is a start day and an end day. The end is exclusive, meaning the range stops just before it. So a rental for days 3 to 5 hands the car back on day 5, and the next rental can start that same morning. A Reservation is a customer, a category, a branch and a date range. Each car now has a category, and the branch it belongs to.

A booking fits if, on every day of its range, the branch has more cars of that category than there are bookings covering that day.

The tempting shortcut is to count every booking that overlaps the range. It’s wrong. Take a branch with two SUVs. Ana has booked days 1 to 3, and Ben days 5 to 7. Now Cy asks for days 2 to 6. Both existing bookings overlap Cy’s range, so the shortcut counts two, sees a full branch, and refuses. But Ana is gone by day 3, and Ben doesn’t start until day 5. On every day of Cy’s range, at most one SUV is out, so Cy fits.

Stage 2: two SUVs, and a new booking for days 2 to 6

SUVs already booked on each day of Cy's range

every day has room, so it fits

would wrongly refuse

day 2: Ana, 1 of 2

day 3: none

day 4: none

day 5: Ben, 1 of 2

Cy: days 2 to 6

Overlap count: Ana and Ben = 2 of 2

SUVs already booked on each day of Cy's range

every day has room, so it fits

would wrongly refuse

day 2: Ana, 1 of 2

day 3: none

day 4: none

day 5: Ben, 1 of 2

Cy: days 2 to 6

Overlap count: Ana and Ben = 2 of 2

Scroll or pinch to zoom, drag to pan

Checking each day costs the number of days times the number of bookings. That’s tiny for a branch: a week’s booking against a few hundred bookings is a few thousand comparisons.

Stage 2: reservations by category and branch

RentalService

-cars: List<Car>

-reservations: Map<String, Reservation>

+reserve(customer, category, branch, range) : Reservation

+cancel(id) : bool

+available(category, branch, range) : bool

Car

+id

+category: Category

+branch

Reservation

+id

+customer

+category: Category

+branch

+range: DateRange

+status: Status

«value»

DateRange

+start

+end

+days()

+contains(day)

«enum»

Category

ECONOMY SUV VAN

RentalService

-cars: List<Car>

-reservations: Map<String, Reservation>

+reserve(customer, category, branch, range) : Reservation

+cancel(id) : bool

+available(category, branch, range) : bool

Car

+id

+category: Category

+branch

Reservation

+id

+customer

+category: Category

+branch

+range: DateRange

+status: Status

«value»

DateRange

+start

+end

+days()

+contains(day)

«enum»

Category

ECONOMY SUV VAN

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation
  • New in this step
RentalService.javarental_service.pyRentalService.csrental_service.gorental-service.ts
// What a customer books: a kind of car, not a particular one.
enum Category { ECONOMY, SUV, VAN }
// Days as numbers counted from some first day, so the arithmetic stays simple. The end is
// exclusive: [3, 5) is a two-day rental that hands the car back on day 5, and another rental
// can start that same day.
record DateRange(int start, int end) {
DateRange {
if (end <= start) throw new IllegalArgumentException("a rental is at least one day");
}
int days() { return end - start; }
boolean contains(int day) { return day >= start && day < end; }
}
class Car {
final String id;
final Category category;
String branch;
Car(String id, Category category, String branch) {
this.id = id;
this.category = category;
this.branch = branch;
}
}
enum Status { RESERVED, CANCELLED }
// A reservation is for a category at a branch, not for a car. Which car the customer drives
// away in is decided when they arrive.
class Reservation {
final String id;
final String customer;
final Category category;
final String branch;
final DateRange range;
Status status = Status.RESERVED;
Reservation(String id, String customer, Category category, String branch, DateRange range) {
this.id = id;
this.customer = customer;
this.category = category;
this.branch = branch;
this.range = range;
}
boolean holdsACar() { return status == Status.RESERVED; }
}
class RentalService {
private final List<Car> cars = new ArrayList<>();
private final Map<String, Reservation> reservations = new LinkedHashMap<>();
private int nextId = 1;
void addCar(String id, Category category, String branch) { cars.add(new Car(id, category, branch)); }
// Returns null when every car of that category at that branch is taken on some day of the
// range.
Reservation reserve(String customer, Category category, String branch, DateRange range) {
if (!available(category, branch, range)) return null;
Reservation r = new Reservation("R" + nextId++, customer, category, branch, range);
reservations.put(r.id, r);
return r;
}
boolean cancel(String id) {
Reservation r = reservations.get(id);
if (r == null || r.status != Status.RESERVED) return false;
r.status = Status.CANCELLED;
return true;
}
Reservation get(String id) { return reservations.get(id); }
// Free on every day of the range: for each day, fewer reservations than cars. Counting every
// reservation that overlaps the range would be wrong: two that overlap it on different days
// need only one car between them.
boolean available(Category category, String branch, DateRange range) {
long fleet = cars.stream().filter(c -> c.category == category && c.branch.equals(branch)).count();
for (int day = range.start(); day < range.end(); day++) {
int d = day;
long booked = reservations.values().stream()
.filter(r -> r.holdsACar() && r.category == category && r.branch.equals(branch) && r.range.contains(d))
.count();
if (booked >= fleet) return false;
}
return true;
}
}
Go deeperChecking Cy's booking, day by day1 min

Cy wants days 2 to 6, which is days 2, 3, 4 and 5 (the end is exclusive). The branch has two SUVs.

Day Ana (1 to 3) Ben (5 to 7) Booked Room for Cy?
2 out 1 Yes
3 0 Yes
4 0 Yes
5 out 1 Yes

Every day has room, so the booking fits. If a third customer, Di, already had days 4 to 6, then day 5 would have two SUVs out, and Cy would be refused. It’s the busiest day that decides, not the number of bookings nearby.

Stage 3: Pickup, return, and the reservation’s life

Section titled “Stage 3: Pickup, return, and the reservation’s life”
Interviewerminute 11

The customer arrives. Which car do they get? And what happens when they bring it back two days late, to a different branch?

What they are testing

Whether you model the reservation’s states and decide when the car is chosen. The red flag is a booking that can be picked up twice, or cancelled while the car is out.

A reservation now moves through a small set of states. It’s reserved when booked, becomes active at pickup, and returned when the car comes back. It can be cancelled only while it’s reserved. Every operation checks the state first, and refuses with WRONG_STATE otherwise. This is the State pattern at its simplest: an enum and a check, because the rules are few.

Stage 3: a reservation's states

reserve

cancel

pickup on the first day, a car is on the lot

give back, any day, any branch

Reserved

Cancelled

Active

Returned

reserve

cancel

pickup on the first day, a car is on the lot

give back, any day, any branch

Reserved

Cancelled

Active

Returned

Scroll or pinch to zoom, drag to pan
  • Color key
  • State
  • Ends well

Pickup is where a reservation becomes a car. On the first day, the service takes any car of the category that is at the branch and not out on a rental, and records it on the reservation. Return works on any day, at any branch. The reservation records the day and the branch, which pricing needs. The car now belongs to the branch it was returned to.

Why choose the car at pickup, not at booking? Because then a car that breaks down, comes back late, or ends up at another branch doesn’t break any booking. Any other car of the category will do. Hotels make the same choice: you book a room type, and get the room number at check-in.

Both awkward cases follow from this. A late return keeps a car out past its booking. The availability check counted that car as free from the booking’s end, so the next customer may arrive to find no car on the lot. Pickup then says NO_CAR, and the counter staff upgrade the customer or call another branch. A one-way return moves a car between branches, so one branch’s fleet grows and the other’s shrinks. Both are real business problems with business answers. What the design must do is notice them, not hide them.

Go deeperWhen the promised car isn't on the lot2 min

Let’s follow the late-return case through, because interviewers like to push on it.

  1. The airport branch has two SUVs. Ana has one for days 1 to 5. Ben has booked the other for days 3 to 6. Cy has booked days 5 to 8. On day 5, the count says one SUV is free for Cy: Ana’s car.
  2. Ana doesn’t come back on day 5. Her reservation is still active.
  3. Cy arrives on day 5. Pickup looks for an SUV at the branch that isn’t out on a rental. Ana’s is out, and Ben’s is out. Pickup returns NO_CAR.
  4. The booking itself is fine: it’s still reserved, and the counter can act. The usual answers are an upgrade (give Cy a van at the SUV price), a car moved from a nearby branch, or a voucher if nothing works.
  5. When Ana returns on day 7, LateFee charges her two late days at the higher rate. That higher rate exists precisely because her lateness cost someone else a car.

Could the design prevent this? Only by keeping a spare car of every category at every branch, which costs real money every day. Rental companies choose to handle it at the counter instead. The design must make the problem visible at the moment it happens, and NO_CAR does that.

Stage 3: a reservation's lifecycle

RentalService

+reserve(customer, category, branch, range) : Reservation

+cancel(id) : Result

+pickup(id, today) : Result

+giveBack(id, today, branch) : Result

Reservation

+status: Status

+car: Car

+returnedOn

+returnBranch

+holdsACar() : bool

Car

+branch

+rental: Reservation

«enum»

Status

RESERVED ACTIVE RETURNED CANCELLED

«enum»

Result

OK NOT_FOUND WRONG_STATE NOT_TODAY NO_CAR

RentalService

+reserve(customer, category, branch, range) : Reservation

+cancel(id) : Result

+pickup(id, today) : Result

+giveBack(id, today, branch) : Result

Reservation

+status: Status

+car: Car

+returnedOn

+returnBranch

+holdsACar() : bool

Car

+branch

+rental: Reservation

«enum»

Status

RESERVED ACTIVE RETURNED CANCELLED

«enum»

Result

OK NOT_FOUND WRONG_STATE NOT_TODAY NO_CAR

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation
  • New in this step
RentalService.javarental_service.pyRentalService.csrental_service.gorental-service.ts
// A reservation's life: booked, picked up, returned. It can be cancelled only before pickup.
enum Status { RESERVED, ACTIVE, RETURNED, CANCELLED }
// What a request gets back. NOT_TODAY: pickup is only on the first day. NO_CAR: the booking
// counted a car, but none is on the lot right now.
enum Result { OK, NOT_FOUND, WRONG_STATE, NOT_TODAY, NO_CAR }
// One car in the fleet. Its branch changes when it is returned somewhere else.
class Car {
final String id;
final Category category;
String branch;
Reservation rental; // the rental using it now, or null while it is on the lot
Car(String id, Category category, String branch) {
this.id = id;
this.category = category;
this.branch = branch;
}
}
// A booking for a kind of car at a branch, for some days. The actual car is chosen at pickup,
// so any free car of that kind will do.
class Reservation {
final String id;
final String customer;
final Category category;
final String branch;
final DateRange range;
Status status = Status.RESERVED;
Car car; // set at pickup
int returnedOn; // set at return
String returnBranch; // set at return; differs from branch for a one-way rental
Reservation(String id, String customer, Category category, String branch, DateRange range) {
this.id = id;
this.customer = customer;
this.category = category;
this.branch = branch;
this.range = range;
}
// Reserved and active rentals need a car. Cancelled and returned ones don't.
boolean holdsACar() { return status == Status.RESERVED || status == Status.ACTIVE; }
}
// The fleet and the bookings. Its main question: on every day of this range, is a car of this
// kind still left at this branch?
class RentalService {
private final List<Car> cars = new ArrayList<>();
private final Map<String, Reservation> reservations = new LinkedHashMap<>();
private int nextId = 1;
void addCar(String id, Category category, String branch) { cars.add(new Car(id, category, branch)); }
// Returns null when no car is free for every day of the range.
Reservation reserve(String customer, Category category, String branch, DateRange range) {
if (!available(category, branch, range)) return null;
Reservation r = new Reservation("R" + nextId++, customer, category, branch, range);
reservations.put(r.id, r);
return r;
}
Result cancel(String id) {
Reservation r = reservations.get(id);
if (r == null) return Result.NOT_FOUND;
if (r.status != Status.RESERVED) return Result.WRONG_STATE;
r.status = Status.CANCELLED;
return Result.OK;
}
// The customer arrives on the first day. Any car of the category that is on this branch's lot
// will do; this is where a reservation becomes a car.
Result pickup(String id, int today) {
Reservation r = reservations.get(id);
if (r == null) return Result.NOT_FOUND;
if (r.status != Status.RESERVED) return Result.WRONG_STATE;
if (today != r.range.start()) return Result.NOT_TODAY;
Car car = cars.stream()
.filter(c -> c.category == r.category && c.branch.equals(r.branch) && c.rental == null)
.findFirst().orElse(null);
if (car == null) return Result.NO_CAR; // a car came back late, or went one way
car.rental = r;
r.car = car;
r.status = Status.ACTIVE;
return Result.OK;
}
// Any day, at any branch. A car returned elsewhere now belongs to that branch's lot.
Result giveBack(String id, int today, String branch) {
Reservation r = reservations.get(id);
if (r == null) return Result.NOT_FOUND;
if (r.status != Status.ACTIVE) return Result.WRONG_STATE;
r.status = Status.RETURNED;
r.returnedOn = today;
r.returnBranch = branch;
r.car.rental = null;
r.car.branch = branch;
return Result.OK;
}
Reservation get(String id) { return reservations.get(id); }
// For each day, compare the cars this branch has with the bookings that need one that day. One
// full day is enough to say no.
boolean available(Category category, String branch, DateRange range) {
long fleet = cars.stream().filter(c -> c.category == category && c.branch.equals(branch)).count();
for (int day = range.start(); day < range.end(); day++) {
int d = day;
long booked = reservations.values().stream()
.filter(r -> r.holdsACar() && r.category == category && r.branch.equals(branch) && r.range.contains(d))
.count();
if (booked >= fleet) return false;
}
return true;
}
}
Go deeperWhen to choose the car, side by side1 min
At booking At pickup (our choice)
Availability check An overlap check per car A day-by-day count per category
A car breaks down or comes back late Its bookings must be moved by hand Any other car of the category will do
One-way returns Future bookings on that car break The branch’s count changes; bookings stand
The customer wants a specific car Possible Not without an extra rule
Interviewerminute 17

A week costs less per day than a weekend. One-way rentals cost extra. Late returns are charged at one and a half times the rate. Marketing will add more. Where does that logic live?

What they are testing

Whether you keep pricing out of the reservation and the service, and make each charge its own rule. The red flag is a calculatePrice method that grows an if for every promotion.

Each kind of charge becomes a PriceRule. Given the facts of a rental, it returns zero or more lines for the bill. The facts are a Charge: the category, the booked range, the day the car came back, and the pickup and return branches. Four rules cover what we need:

  • DailyRate charges each booked day at the category’s rate.
  • LongRentalDiscount takes a percentage off when the booking is a week or longer.
  • OneWayFee adds a fee when the return branch differs from the pickup branch.
  • LateFee charges each day past the booked end at a higher rate, because the car may have been promised to someone else.

Pricing runs every rule, and collects the lines into a Bill. Before the trip, it builds a Charge from the plan (returning on time, at the same branch), and that’s the quote. After the trip, it builds one from what actually happened, and that’s the invoice. The same rules make both, so a customer who returns on time to the same branch pays exactly what they were quoted. Money is kept in cents, so the lines add up exactly.

Each rule is swappable, and new ones don’t touch the others. That’s the Strategy pattern.

Stage 4: pricing rules

Pricing

-rules: List<PriceRule>

+quote(Reservation) : Bill

+invoice(Reservation) : Bill

«interface»

PriceRule

+apply(Charge) : List<Line>

DailyRate

LongRentalDiscount

OneWayFee

LateFee

«value»

Charge

+category

+range

+returnedOn

+pickupBranch

+returnBranch

«value»

Bill

+lines: List<Line>

+total()

Pricing

-rules: List<PriceRule>

+quote(Reservation) : Bill

+invoice(Reservation) : Bill

«interface»

PriceRule

+apply(Charge) : List<Line>

DailyRate

LongRentalDiscount

OneWayFee

LateFee

«value»

Charge

+category

+range

+returnedOn

+pickupBranch

+returnBranch

«value»

Bill

+lines: List<Line>

+total()

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation
  • New in this step
Pricing.javacharge.pyPricing.cspricing.gopricing.ts
// Money in cents, so totals add up exactly.
record Line(String label, long cents) {}
// A bill keeps its lines, not just the total, so the customer sees what each charge is for.
record Bill(List<Line> lines) {
long total() { return lines.stream().mapToLong(Line::cents).sum(); }
}
// What a price rule can see. Before pickup it's a quote: returnedOn is the planned end and
// returnBranch the pickup branch. At return it's the real thing.
record Charge(Category category, DateRange range, int returnedOn, String pickupBranch, String returnBranch) {
int daysCharged() { return Math.max(range.days(), returnedOn - range.start()); }
int lateDays() { return Math.max(0, returnedOn - range.end()); }
}
// One rule per kind of charge. A new fee, a promotion or a different rate card is a new rule,
// and the service that adds up the bill doesn't change.
interface PriceRule {
List<Line> apply(Charge c);
}
// The base price: booked days times the category's daily rate.
record DailyRate(Map<Category, Long> perDay) implements PriceRule {
public List<Line> apply(Charge c) {
return List.of(new Line(c.range().days() + " days of " + c.category(), c.range().days() * perDay.get(c.category())));
}
}
// Long rentals pay less per day: a percentage off the booked days.
record LongRentalDiscount(int minDays, int percent, Map<Category, Long> perDay) implements PriceRule {
public List<Line> apply(Charge c) {
if (c.range().days() < minDays) return List.of();
long off = c.range().days() * perDay.get(c.category()) * percent / 100;
return List.of(new Line(percent + "% off " + minDays + "+ days", -off));
}
}
// A car left at another branch has to be brought back, so a one-way rental pays a flat fee.
record OneWayFee(long cents) implements PriceRule {
public List<Line> apply(Charge c) {
return c.pickupBranch().equals(c.returnBranch()) ? List.of() : List.of(new Line("one-way", cents));
}
}
// Each day late costs more than a booked day, because the car may have been promised to the
// next customer.
record LateFee(int percentOfRate, Map<Category, Long> perDay) implements PriceRule {
public List<Line> apply(Charge c) {
if (c.lateDays() == 0) return List.of();
long cents = c.lateDays() * perDay.get(c.category()) * percentOfRate / 100;
return List.of(new Line(c.lateDays() + " days late", cents));
}
}
// Runs every rule and collects their lines into one bill. The order of the rules is the order
// of the lines on the bill.
class Pricing {
private final List<PriceRule> rules;
Pricing(List<PriceRule> rules) { this.rules = rules; }
Bill bill(Charge c) {
List<Line> lines = new ArrayList<>();
for (PriceRule rule : rules) lines.addAll(rule.apply(c));
return new Bill(lines);
}
// The same rules, run with the plan instead of what happened.
Bill quote(Reservation r) { return bill(new Charge(r.category, r.range, r.range.end(), r.branch, r.branch)); }
// The same rules, run with what really happened: the return day and branch.
Bill invoice(Reservation r) { return bill(new Charge(r.category, r.range, r.returnedOn, r.branch, r.returnBranch)); }
}
Go deeperA late, one-way rental, line by line2 min

Say an SUV costs 60.00 a day, the one-way fee is 50.00, and late days cost one and a half times the rate. Ana books days 1 to 8 at the airport: seven days. She returns on day 10, at the city branch.

The quote, at booking, assumes an on-time return to the same branch:

Rule Line Amount
DailyRate 7 days × 60.00 420.00
LongRentalDiscount a week or more: 10% off 420.00 −42.00
OneWayFee same branch: no line
LateFee on time: no line
Total 378.00

The invoice, after the trip, uses what happened: two days late, and a different branch.

Rule Line Amount
DailyRate 7 days × 60.00 420.00
LongRentalDiscount 10% off 420.00 −42.00
OneWayFee airport to city 50.00
LateFee 2 days × 90.00 180.00
Total 608.00

The first two lines are identical in both, because the same rules made them from the same booked range. Ana can see exactly which line changed and why, which is what customers ask about when a bill is higher than the quote. (These are the same rates and fees the tests use.)

A weekend surcharge or a coupon is one more rule in the list. Pricing, the reservation and the service don’t change.

This is enough for a mid-level answer. Bookings by category and branch, checked day by day, a reservation with states, and pricing as rules. The rest of the page is what makes it a senior answer.

Here is everything together. Bookings is what the website, the app and the counter call. It uses RentalService to keep cars and reservations, and Pricing to quote and invoice.

Car rental: full class diagram

Bookings

-lock

+book(customer, category, branch, range) : Reservation

+pickup(id, today) : Result

+giveBack(id, today, branch) : Result

RentalService

+reserve(customer, category, branch, range) : Reservation

+cancel(id) : Result

+pickup(id, today) : Result

+giveBack(id, today, branch) : Result

+available(category, branch, range) : bool

«interface»

Payments

+charge(customer, cents) : bool

Pricing

+quote(Reservation) : Bill

+invoice(Reservation) : Bill

«interface»

PriceRule

+apply(Charge) : List<Line>

Reservation

+category

+branch

+range: DateRange

+status: Status

+car: Car

Car

+category

+branch

«value»

DateRange

«value»

Bill

«enum»

Status

Bookings

-lock

+book(customer, category, branch, range) : Reservation

+pickup(id, today) : Result

+giveBack(id, today, branch) : Result

RentalService

+reserve(customer, category, branch, range) : Reservation

+cancel(id) : Result

+pickup(id, today) : Result

+giveBack(id, today, branch) : Result

+available(category, branch, range) : bool

«interface»

Payments

+charge(customer, cents) : bool

Pricing

+quote(Reservation) : Bill

+invoice(Reservation) : Bill

«interface»

PriceRule

+apply(Charge) : List<Line>

Reservation

+category

+branch

+range: DateRange

+status: Status

+car: Car

Car

+category

+branch

«value»

DateRange

«value»

Bill

«enum»

Status

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation
car-rental· read only
AllInOne.java
AllInOne.java
// The complete design, built from every stage on this page.
import java.util.*;
// One car in the fleet. Its branch changes when it is returned somewhere else.
class Car {
final String id;
final Category category;
String branch;
Reservation rental; // the rental using it now, or null while it is on the lot
Car(String id, Category category, String branch) {
this.id = id;
this.category = category;
this.branch = branch;
}
}
// The fleet and the bookings. Its main question: on every day of this range, is a car of this
// kind still left at this branch?
class RentalService {
private final List<Car> cars = new ArrayList<>();
private final Map<String, Reservation> reservations = new LinkedHashMap<>();
private int nextId = 1;
void addCar(String id, Category category, String branch) { cars.add(new Car(id, category, branch)); }
// Returns null when no car is free for every day of the range.
Reservation reserve(String customer, Category category, String branch, DateRange range) {
if (!available(category, branch, range)) return null;
Reservation r = new Reservation("R" + nextId++, customer, category, branch, range);
reservations.put(r.id, r);
return r;
}
Result cancel(String id) {
Reservation r = reservations.get(id);
if (r == null) return Result.NOT_FOUND;
if (r.status != Status.RESERVED) return Result.WRONG_STATE;
r.status = Status.CANCELLED;
return Result.OK;
}
// The customer arrives on the first day. Any car of the category that is on this branch's lot
// will do; this is where a reservation becomes a car.
Result pickup(String id, int today) {
Reservation r = reservations.get(id);
if (r == null) return Result.NOT_FOUND;
if (r.status != Status.RESERVED) return Result.WRONG_STATE;
if (today != r.range.start()) return Result.NOT_TODAY;
Car car = cars.stream()
.filter(c -> c.category == r.category && c.branch.equals(r.branch) && c.rental == null)
.findFirst().orElse(null);
if (car == null) return Result.NO_CAR; // a car came back late, or went one way
car.rental = r;
r.car = car;
r.status = Status.ACTIVE;
return Result.OK;
}
// Any day, at any branch. A car returned elsewhere now belongs to that branch's lot.
Result giveBack(String id, int today, String branch) {
Reservation r = reservations.get(id);
if (r == null) return Result.NOT_FOUND;
if (r.status != Status.ACTIVE) return Result.WRONG_STATE;
r.status = Status.RETURNED;
r.returnedOn = today;
r.returnBranch = branch;
r.car.rental = null;
r.car.branch = branch;
return Result.OK;
}
Reservation get(String id) { return reservations.get(id); }
// For each day, compare the cars this branch has with the bookings that need one that day. One
// full day is enough to say no.
boolean available(Category category, String branch, DateRange range) {
long fleet = cars.stream().filter(c -> c.category == category && c.branch.equals(branch)).count();
for (int day = range.start(); day < range.end(); day++) {
int d = day;
long booked = reservations.values().stream()
.filter(r -> r.holdsACar() && r.category == category && r.branch.equals(branch) && r.range.contains(d))
.count();
if (booked >= fleet) return false;
}
return true;
}
}
// What a customer books: a kind of car, not a particular one.
enum Category { ECONOMY, SUV, VAN }
// Days as numbers counted from some first day, so the arithmetic stays simple. The end is
// exclusive: [3, 5) is a two-day rental that hands the car back on day 5, and another rental
// can start that same day.
record DateRange(int start, int end) {
DateRange {
if (end <= start) throw new IllegalArgumentException("a rental is at least one day");
}
int days() { return end - start; }
boolean contains(int day) { return day >= start && day < end; }
}
// A reservation's life: booked, picked up, returned. It can be cancelled only before pickup.
enum Status { RESERVED, ACTIVE, RETURNED, CANCELLED }
// A booking for a kind of car at a branch, for some days. The actual car is chosen at pickup,
// so any free car of that kind will do.
class Reservation {
final String id;
final String customer;
final Category category;
final String branch;
final DateRange range;
Status status = Status.RESERVED;
Car car; // set at pickup
int returnedOn; // set at return
String returnBranch; // set at return; differs from branch for a one-way rental
Reservation(String id, String customer, Category category, String branch, DateRange range) {
this.id = id;
this.customer = customer;
this.category = category;
this.branch = branch;
this.range = range;
}
// Reserved and active rentals need a car. Cancelled and returned ones don't.
boolean holdsACar() { return status == Status.RESERVED || status == Status.ACTIVE; }
}
// What a request gets back. NOT_TODAY: pickup is only on the first day. NO_CAR: the booking
// counted a car, but none is on the lot right now.
enum Result { OK, NOT_FOUND, WRONG_STATE, NOT_TODAY, NO_CAR }
// Money in cents, so totals add up exactly.
record Line(String label, long cents) {}
// A bill keeps its lines, not just the total, so the customer sees what each charge is for.
record Bill(List<Line> lines) {
long total() { return lines.stream().mapToLong(Line::cents).sum(); }
}
// What a price rule can see. Before pickup it's a quote: returnedOn is the planned end and
// returnBranch the pickup branch. At return it's the real thing.
record Charge(Category category, DateRange range, int returnedOn, String pickupBranch, String returnBranch) {
int daysCharged() { return Math.max(range.days(), returnedOn - range.start()); }
int lateDays() { return Math.max(0, returnedOn - range.end()); }
}
// One rule per kind of charge. A new fee, a promotion or a different rate card is a new rule,
// and the service that adds up the bill doesn't change.
interface PriceRule {
List<Line> apply(Charge c);
}
// The base price: booked days times the category's daily rate.
record DailyRate(Map<Category, Long> perDay) implements PriceRule {
public List<Line> apply(Charge c) {
return List.of(new Line(c.range().days() + " days of " + c.category(), c.range().days() * perDay.get(c.category())));
}
}
// Long rentals pay less per day: a percentage off the booked days.
record LongRentalDiscount(int minDays, int percent, Map<Category, Long> perDay) implements PriceRule {
public List<Line> apply(Charge c) {
if (c.range().days() < minDays) return List.of();
long off = c.range().days() * perDay.get(c.category()) * percent / 100;
return List.of(new Line(percent + "% off " + minDays + "+ days", -off));
}
}
// A car left at another branch has to be brought back, so a one-way rental pays a flat fee.
record OneWayFee(long cents) implements PriceRule {
public List<Line> apply(Charge c) {
return c.pickupBranch().equals(c.returnBranch()) ? List.of() : List.of(new Line("one-way", cents));
}
}
// Each day late costs more than a booked day, because the car may have been promised to the
// next customer.
record LateFee(int percentOfRate, Map<Category, Long> perDay) implements PriceRule {
public List<Line> apply(Charge c) {
if (c.lateDays() == 0) return List.of();
long cents = c.lateDays() * perDay.get(c.category()) * percentOfRate / 100;
return List.of(new Line(c.lateDays() + " days late", cents));
}
}
// Runs every rule and collects their lines into one bill. The order of the rules is the order
// of the lines on the bill.
class Pricing {
private final List<PriceRule> rules;
Pricing(List<PriceRule> rules) { this.rules = rules; }
Bill bill(Charge c) {
List<Line> lines = new ArrayList<>();
for (PriceRule rule : rules) lines.addAll(rule.apply(c));
return new Bill(lines);
}
// The same rules, run with the plan instead of what happened.
Bill quote(Reservation r) { return bill(new Charge(r.category, r.range, r.range.end(), r.branch, r.branch)); }
// The same rules, run with what really happened: the return day and branch.
Bill invoice(Reservation r) { return bill(new Charge(r.category, r.range, r.returnedOn, r.branch, r.returnBranch)); }
}
// Payment, which takes a second or two and may decline.
interface Payments {
boolean charge(String customer, long cents);
}
// Many customers book at once, from the website, the app and the counter. Checking that a car
// is free and recording the reservation must be one step, or two customers can both see the
// last SUV as free and both book it. That step happens under the lock. Charging the card is
// slow, so it happens outside the lock: the reservation is recorded first, which holds the car,
// and cancelled if the card is declined.
class Bookings {
private final RentalService service;
private final Pricing pricing;
private final Payments payments;
private final Object lock = new Object();
Bookings(RentalService service, Pricing pricing, Payments payments) {
this.service = service;
this.pricing = pricing;
this.payments = payments;
}
Reservation book(String customer, Category category, String branch, DateRange range) {
Reservation r;
synchronized (lock) {
r = service.reserve(customer, category, branch, range);
}
if (r == null) return null;
if (payments.charge(customer, pricing.quote(r).total())) return r;
synchronized (lock) {
service.cancel(r.id);
}
return null;
}
// Pickup and return change which cars are free, so they take the same lock.
Result pickup(String id, int today) {
synchronized (lock) {
return service.pickup(id, today);
}
}
Result giveBack(String id, int today, String branch) {
synchronized (lock) {
return service.giveBack(id, today, branch);
}
}
boolean available(Category category, String branch, DateRange range) {
synchronized (lock) {
return service.available(category, branch, range);
}
}
}
Read onlyJava 21Every file builds in CI19 files267 lines
all_in_one.py
all_in_one.py
Read onlyPython 3.12Every file builds in CI19 files306 lines
AllInOne.cs
AllInOne.cs
Read onlyC# · .NET 10Every file builds in CI19 files253 lines
all_in_one.go
all_in_one.go
Read onlyGo 1.24Every file builds in CI19 files344 lines
all-in-one.ts
all-in-one.ts
Read onlyTypeScript, strictEvery file builds in CI20 files293 lines

Three ideas hold it together:

  • Promises are counts, and cars are chosen late. Booking checks numbers per day. A real car is picked only at the counter.
  • The reservation records facts, not money. Pricing reads the facts, so the quote and the invoice come from the same rules (Strategy).
  • Every change to a reservation checks its state (State). A booking can’t be picked up twice, cancelled while the car is out, or returned before pickup. Callers book and pay in one call, through Bookings (Facade).

So far, one booking happens at a time. Now the website, the app and the counter all book at once.

Interviewerminute 23

Bookings come from the website, the app and the counter at the same time. Two customers try to book the last SUV for the same weekend. What happens?

What they are testing

Whether you see the check-then-act race, and keep slow work out of the lock. The red flag is holding a lock while calling the payment provider.

reserve checks availability, then records the booking. Picture two requests:

  1. Request A checks: one SUV free.
  2. Request B checks: one SUV free.
  3. A records its booking. B records its booking.

The branch has now promised two cars it doesn’t have. This is a check-then-act race: the check was true when it ran, but not when the action happened.

The clearly correct fix: checking and recording happen as one step, under one lock. Then B’s check runs after A’s booking is recorded, and B sees no SUV free.

The better fix keeps payment out of the lock. Charging a card takes a second or two, and can be declined. Holding the lock while it runs would make every other booking, at every branch, wait. So the booking is recorded first, under the lock, which holds the car. Then the card is charged after, with no lock. If the card is declined, the booking is cancelled, under the lock again, and the car is free for the next customer.

Sequence: two customers, one SUV left
PaymentsBookingsBenAnaPaymentsBookingsBenAnahad the charge been declined, R7 is cancelled and the SUV is free againbook SUV, days 3 to 5lock, check, record R7, unlockbook SUV, days 3 to 5lock, check finds no SUV free, unlocknone availablecharge Ana (no lock held)approvedbooked R7PaymentsBookingsBenAnaPaymentsBookingsBenAnahad the charge been declined, R7 is cancelled and the SUV is free againbook SUV, days 3 to 5lock, check, record R7, unlockbook SUV, days 3 to 5lock, check finds no SUV free, unlocknone availablecharge Ana (no lock held)approvedbooked R7
Scroll or pinch to zoom, drag to pan
  • Color key
  • Interface
  • Facade
Option How Overbooks? Cost
1. Lock, record, then pay outside the lock (our choice) Hold the car, then charge No A declined card holds a car for a second
2. Lock held while paying Simplest to reason about No Every booking waits on the card network
3. Pay first, then book Charge, then try to reserve No A refund when the car is gone by then
4. A lock per branch and category Bookings for different branches don’t wait No More locks; needed only at large volume
5. A database rule A row per car per day, unique; insert fails when full No The real answer at scale; see below
Go deeperWhy not charge the card first?1 min

Charging first sounds safer: no money, no booking. But think about the last SUV again. Both customers’ cards are charged, which takes a second or two each. Then both try to book, and one finds the SUV gone. Now we must refund a customer who was charged for nothing, and refunds take days to reach their bank. That’s a worse experience than a card being declined.

Holding the car first flips it around. Only a customer who has a car is ever charged. If their card is declined, they lose nothing, and the car goes back into the count a second later. The order of the two steps is a design decision, and “hold, then charge” is the one that keeps the bad case cheap.

Bookings.javabookings.pyBookings.csbookings.gobookings.ts
// Payment, which takes a second or two and may decline.
interface Payments {
boolean charge(String customer, long cents);
}
// Many customers book at once, from the website, the app and the counter. Checking that a car
// is free and recording the reservation must be one step, or two customers can both see the
// last SUV as free and both book it. That step happens under the lock. Charging the card is
// slow, so it happens outside the lock: the reservation is recorded first, which holds the car,
// and cancelled if the card is declined.
class Bookings {
private final RentalService service;
private final Pricing pricing;
private final Payments payments;
private final Object lock = new Object();
Bookings(RentalService service, Pricing pricing, Payments payments) {
this.service = service;
this.pricing = pricing;
this.payments = payments;
}
Reservation book(String customer, Category category, String branch, DateRange range) {
Reservation r;
synchronized (lock) {
r = service.reserve(customer, category, branch, range);
}
if (r == null) return null;
if (payments.charge(customer, pricing.quote(r).total())) return r;
synchronized (lock) {
service.cancel(r.id);
}
return null;
}
// Pickup and return change which cars are free, so they take the same lock.
Result pickup(String id, int today) {
synchronized (lock) {
return service.pickup(id, today);
}
}
Result giveBack(String id, int today, String branch) {
synchronized (lock) {
return service.giveBack(id, today, branch);
}
}
boolean available(Category category, String branch, DateRange range) {
synchronized (lock) {
return service.available(category, branch, range);
}
}
}

Why is this correct? Every booking’s check and record happen together under the lock, so every check sees every booking recorded before it. No day can ever have more bookings than cars.

What does it cost? One lock for all bookings, held only for a quick count. A declined card holds a car for a second or two, which is harmless.

Go deeperHow each language does it1 min

In Java, Python, C# and Go, requests run on their own threads, so the lock is needed. TypeScript runs one callback at a time, so reserve, which never waits with await, is already one step. Only the card charge waits, and it comes after the car is held.

Every operation takes the day as an argument, so a test moves through a whole rental in a few lines, without waiting for real days to pass. The tests that matter:

  • Bookings fill a branch until some day has no car left, and fit again once the cars are back.
  • Two bookings on different days share one car, even though both overlap a third.
  • Cancelling frees the days, and cancelling twice is refused.
  • Pickup works only on the first day, gives a car of the right category, and only once.
  • A one-way return moves the car to the other branch’s count.
  • A late return leaves the next customer without a car until it comes back.
  • A week’s quote is the daily rate with the long-rental discount.
  • An invoice for a late, one-way return adds both fees.
  • A declined card cancels the booking and frees the car.
  • Eight customers booking two SUVs at once get exactly two bookings.
Go deeperThe tests in five languages2 min

The last test fails within a run in Java, Python and Go if the lock is removed: three or more bookings go through.

Main.javadecline.pyFixedPayments.csapprove.goexample.ts
static final Map<Category, Long> RATES = Map.of(Category.ECONOMY, 3000L, Category.SUV, 6000L, Category.VAN, 8000L);
static Pricing pricing() {
return new Pricing(List.of(
new DailyRate(RATES),
new LongRentalDiscount(7, 10, RATES),
new OneWayFee(5000),
new LateFee(150, RATES)));
}
static RentalService twoSuvsDowntown() {
RentalService s = new RentalService();
s.addCar("suv-1", Category.SUV, "downtown");
s.addCar("suv-2", Category.SUV, "downtown");
s.addCar("eco-1", Category.ECONOMY, "airport");
return s;
}
@Test
void reservesUntilEveryCarIsTakenOnSomeDay() {
RentalService s = twoSuvsDowntown();
assertNotNull(s.reserve("ana", Category.SUV, "downtown", new DateRange(1, 4)));
assertNotNull(s.reserve("ben", Category.SUV, "downtown", new DateRange(2, 5)));
assertNull(s.reserve("cy", Category.SUV, "downtown", new DateRange(3, 4))); // both out on day 3
assertNotNull(s.reserve("cy", Category.SUV, "downtown", new DateRange(5, 6))); // both back by day 5
assertNull(s.reserve("di", Category.SUV, "airport", new DateRange(1, 2))); // no SUVs there
}
@Test
void reservationsOnDifferentDaysShareACar() {
RentalService s = twoSuvsDowntown();
s.reserve("ana", Category.SUV, "downtown", new DateRange(1, 3));
s.reserve("ben", Category.SUV, "downtown", new DateRange(5, 7));
// Both overlap days 2 to 6, but never on the same day, so one car covers them.
assertNotNull(s.reserve("cy", Category.SUV, "downtown", new DateRange(2, 6)));
}
@Test
void cancellingFreesTheDays() {
RentalService s = twoSuvsDowntown();
Reservation a = s.reserve("ana", Category.SUV, "downtown", new DateRange(1, 3));
s.reserve("ben", Category.SUV, "downtown", new DateRange(1, 3));
assertFalse(s.available(Category.SUV, "downtown", new DateRange(2, 3)));
assertEquals(Result.OK, s.cancel(a.id));
assertTrue(s.available(Category.SUV, "downtown", new DateRange(2, 3)));
assertEquals(Result.WRONG_STATE, s.cancel(a.id));
}
@Test
void pickupAssignsACarOnTheFirstDayOnly() {
RentalService s = twoSuvsDowntown();
Reservation r = s.reserve("ana", Category.SUV, "downtown", new DateRange(3, 5));
assertEquals(Result.NOT_TODAY, s.pickup(r.id, 2));
assertEquals(Result.OK, s.pickup(r.id, 3));
assertEquals(Status.ACTIVE, r.status);
assertEquals(Category.SUV, r.car.category);
assertEquals(Result.WRONG_STATE, s.pickup(r.id, 3));
assertEquals(Result.WRONG_STATE, s.cancel(r.id));
}
@Test
void aOneWayReturnMovesTheCar() {
RentalService s = twoSuvsDowntown();
Reservation r = s.reserve("ana", Category.ECONOMY, "airport", new DateRange(1, 2));
s.pickup(r.id, 1);
assertEquals(Result.OK, s.giveBack(r.id, 2, "downtown"));
assertEquals("downtown", r.car.branch);
assertTrue(s.available(Category.ECONOMY, "downtown", new DateRange(3, 4)));
assertFalse(s.available(Category.ECONOMY, "airport", new DateRange(3, 4)));
}
@Test
void aLateReturnLeavesTheNextCustomerWithoutACar() {
RentalService s = new RentalService();
s.addCar("suv-1", Category.SUV, "downtown");
Reservation first = s.reserve("ana", Category.SUV, "downtown", new DateRange(1, 3));
Reservation next = s.reserve("ben", Category.SUV, "downtown", new DateRange(3, 5));
s.pickup(first.id, 1);
assertEquals(Result.NO_CAR, s.pickup(next.id, 3)); // ana hasn't brought it back
s.giveBack(first.id, 3, "downtown");
assertEquals(Result.OK, s.pickup(next.id, 3));
}
@Test
void aQuoteAddsUpTheRules() {
RentalService s = twoSuvsDowntown();
Reservation week = s.reserve("ana", Category.SUV, "downtown", new DateRange(1, 8));
Bill quote = pricing().quote(week);
assertEquals(2, quote.lines().size());
assertEquals(7 * 6000 - 7 * 6000 / 10, quote.total()); // 7 days, 10% off
}
@Test
void theInvoiceChargesForWhatHappened() {
RentalService s = twoSuvsDowntown();
Reservation r = s.reserve("ana", Category.ECONOMY, "airport", new DateRange(1, 3));
s.pickup(r.id, 1);
s.giveBack(r.id, 4, "downtown"); // a day late, and one way
Bill bill = pricing().invoice(r);
assertEquals(2 * 3000 + 5000 + 3000 * 150 / 100, bill.total());
}
@Test
void aDeclinedCardReleasesTheCar() {
RentalService s = twoSuvsDowntown();
Bookings declined = new Bookings(s, pricing(), (customer, cents) -> false);
assertNull(declined.book("ana", Category.ECONOMY, "airport", new DateRange(1, 2)));
assertTrue(s.available(Category.ECONOMY, "airport", new DateRange(1, 2)));
}
@Test
void racingBookingsNeverOverbook() throws Exception {
for (int round = 0; round < 50; round++) {
RentalService s = twoSuvsDowntown();
Bookings bookings = new Bookings(s, pricing(), (customer, cents) -> true);
CountDownLatch start = new CountDownLatch(1);
AtomicInteger booked = new AtomicInteger();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 8; i++) {
String customer = "c" + i;
Thread t = new Thread(() -> {
try {
start.await();
} catch (InterruptedException e) {
return;
}
if (bookings.book(customer, Category.SUV, "downtown", new DateRange(1, 3)) != null) booked.incrementAndGet();
});
t.start();
threads.add(t);
}
start.countDown();
for (Thread t : threads) t.join();
assertEquals(2, booked.get()); // two SUVs, two bookings
}
}
Go deeperEvery edge case, and where it is handled2 min
Case What happens Where
No car of that category at that branch Not available available
Two bookings back to back on the same car Both fit: the end day is exclusive DateRange.contains
A range with no days Refused when the range is made DateRange
Cancel after pickup WRONG_STATE cancel
Pickup on the wrong day NOT_TODAY pickup
Pickup twice WRONG_STATE pickup
The last car came back late NO_CAR at pickup; staff upgrade or move a car pickup
Returned to another branch The car joins that branch; one-way fee giveBack, OneWayFee
Returned late Charged per late day at a higher rate LateFee
Returned early Charged for the booked days DailyRate
Card declined Booking cancelled, car free again Bookings.book
Two customers, one car One booking, one “none available” Bookings lock
Decision We chose We gave up
What is booked A category at a branch for a range of days Choosing a specific car
Availability Checked day by day against the fleet A single overlap count, which is wrong
Choosing the car At pickup, from the lot Knowing the car ahead of time
Late and one-way Allowed, noticed at pickup as NO_CAR A guarantee every booking has a car waiting
Pricing A list of rules producing lines; quote and invoice share them A single price stored at booking
Booking at once Check and record under a lock; pay outside it A car held for a second on a declined card
Go deeperGenuinely different designs, and when to pick them2 min

Book a specific car. Car-sharing services like Zipcar book a particular car at a particular spot, so availability is a simple overlap check per car, and the car is known from the start. It fits when customers choose the car on a map, and loses the flexibility of a pool.

Overbooking on purpose. Some rental companies accept a few percent more bookings than cars, because some customers never show up. Availability becomes booked < fleet × 1.05, and the counter needs a plan for the day everyone does show up.

Event sourcing. Store every booking, pickup and return as an event, and work out availability from them. It gives a full history of what happened, which rental disputes often need, at the cost of building the side that answers questions.

  • 0 to 3. Book ahead or walk in, category or car, branches, one-way returns, pricing.
  • 3 to 5. The promises. Name “never promise more cars than a branch has, on any day” as the hard one.
  • 5 to 22. Renting on the spot, then bookings by category checked day by day, then pickup and return with states, then pricing rules. Code available, pickup and one price rule.
  • 22 to 24. Walk through the full diagram: promises are counts, cars are chosen late.
  • 24 to 29. Two customers and one car: the lock, and paying outside it.
  • 29 to 45. Many servers: a conditional update per day. Then your own questions.

If time is short, give pricing one sentence (“a list of rules that each add lines to a bill”), and spend the time on day-by-day availability and the booking race. Those decide this question for most interviewers.

Interviewerminute 29

We now have 2,000 branches, and the bookings live in a database across many servers. How does availability work there?

What they are testing

Whether you can move the one-step check into the database instead of a lock in memory. A lock in one server’s memory protects nothing once there are two servers.

Most extensions fit into one place:

Extension What changes Touches booking?
Hourly rentals Slots instead of days in the per-day check Yes, the unit
Upgrades at pickup Pickup falls back to a higher category with a car on the lot No
Cars out of service Removed from the fleet count for those days Yes, the count
Moving cars around A job that moves cars between branches before they’re needed No
Coupons, weekend rates More price rules No
Many servers A conditional update per day in the database, or holds that expire Yes
Go deeperMoving to a database and many servers, step by step2 min

With several servers, the lock in memory is gone, so the one step must happen in the database.

  1. Keep an inventory row per branch, category and day. Each row holds the fleet size and how many are booked. A week’s booking touches seven rows.
  2. Book in one transaction, with a condition on every row. For each day: UPDATE inventory SET booked = booked + 1 WHERE branch = ? AND category = ? AND day = ? AND booked < fleet. If any update changes no row, that day is full, and the whole transaction is rolled back. It’s the same day-by-day check, made into one step by the database.
  3. Hold the car, then pay. The booking is written with a status of “held” and an expiry, ten minutes ahead. Payment runs with no transaction open. Success turns the hold into a booking.
  4. Let holds expire. If the server crashes during payment, nobody cancels the booking. A job finds expired holds, and gives their days back by lowering booked. In memory, our code cancelled straight away; with many servers, an expiry is what makes that safe.
  5. Two busy branches don’t block each other. Each transaction locks only its own rows, so bookings at the airport never wait for bookings in the city.

The new costs are a row per branch, category and day (2,000 branches, 5 categories and a year ahead is about 3.6 million small rows), and a job to clean up holds.

Go deeperFour more follow-up questions2 min
Interviewer asksHow do you show a customer which categories are free for their dates?

Call available for each category at the branch. It’s a handful of categories, each a day-by-day count, so it’s fast. With the per-day inventory rows, it’s one query: the categories whose booked count stays under the fleet on every day of the range.

Interviewer asksWhat if the customer wants to extend their rental while they have the car?

Treat it as a booking for the extra days, same category and branch, checked the same way. If it fits, extend the reservation’s range. If it doesn’t, someone else is waiting for a car, and the customer is told to return on time or pay the late fee.

Interviewer asksHow would you handle a car that breaks down during a rental?

Mark the car out of service, and swap it: the reservation stays active and points to a new car from the nearest branch. Because the reservation is for a category, nothing else about it changes. The broken car leaves the fleet count until it’s repaired.

Interviewer asksWhy not let the database pick the car at booking with a unique index?

You can, with a row per car per day, and it makes double-booking a car impossible. But you have to choose a car at booking, which brings back the problems with late and one-way returns. A count per category keeps the same promise, never more bookings than cars, without tying a booking to a car early.