Car rental

Design a car rental service with several branches. Customers reserve a vehicle of a type at a branch, pick it up, and return it, possibly at another branch. The fee depends on the days and the vehicle type, with a late fee.

Design a car rental service. It has branches, and each branch has vehicles of different types, such as hatchback, sedan and SUV. A customer reserves a vehicle of a type at a branch, picks it up, and returns it. The price depends on the number of days and the type. Returning late costs extra.

Questions to ask first

  • Do we reserve for future dates? Assume no. A reservation takes a vehicle out of the free pool now. Date calendars are an extension.
  • Does the customer pick a particular vehicle? Assume no. They pick a type and a branch, and the service gives the free vehicle with the lowest plate.
  • Can a vehicle be returned to another branch? Assume yes. It then joins the free pool of the branch where it was returned.
  • How is the fee worked out? Assume a daily rate per type, charged for the days the customer booked.
  • What is a late fee? Assume each day beyond the booked days costs one and a half times the daily rate.
  • What if the customer returns early? Assume the fee stays the same. A refund rule is an extension.
  • How do we know the day? Assume the caller passes a whole day number to every call. There is no clock.

Requirements

  • Keep vehicles of several types at several branches.
  • Show how many vehicles of a type are free at a branch.
  • Reserve a vehicle of a type at a branch for a number of days, or refuse when none is free.
  • Move a reservation through its states: reserved, picked up, returned, or cancelled before pickup.
  • Charge by daily rate and type, with a late fee for days beyond the booked days.
  • Accept a return at any branch and make the vehicle free there.
  • Refuse any step that does not fit the reservation's state, and change nothing.

Out of scope

  • Reserving for future dates and a calendar per vehicle.
  • Driving licence checks, insurance and deposits.
  • Real payments and invoices.
  • Saving data to a database (in-memory storage is enough).

Entities

  • CarRentalService: reserve(customer, type, branch, days), pickUp(id, day), giveBack(id, branch, day), cancel(id), available(branch, type)
  • Branch: name, free vehicles by type; add(vehicle), take(type), count(type)
  • Vehicle: plate, type
  • VehicleType: HATCHBACK, SEDAN, SUV
  • Reservation: id, customer, vehicle, pickup branch, booked days, status; pickUp(day), cancel(), finish(day)
  • ReservationStatus: RESERVED, PICKED_UP, RETURNED, CANCELLED
  • PricingPolicy: fee(type, bookedDays, actualDays), the rule for the total
  • StandardPricing: a daily rate per type and a late rate

Classes in the model design

  • CarRentalService (class)
  • Branch (class)
  • Vehicle (class)
  • VehicleType (enum)
  • Reservation (class)
  • ReservationStatus (enum)
  • PricingPolicy (interface)
  • StandardPricing (class)

The main use case, step by step

  1. Asha wants a hatchback at the Central branch for 3 days. The app calls `service.reserve("Asha", HATCHBACK, "Central", 3)`.
  2. The service finds the Central `Branch` and calls `take(HATCHBACK)`. The branch removes the free hatchback with the lowest plate, H1, from its pool under its own lock, so two customers cannot get the same car.
  3. The service creates a `Reservation` R1 in the state RESERVED, holding H1, and returns "R1". Central now has one free hatchback.
  4. On day 10, `pickUp("R1", 10)` calls `reservation.pickUp(10)`. The reservation checks that its state is RESERVED, records day 10 and moves to PICKED_UP.
  5. On day 13, `giveBack("R1", "Central", 13)` calls `reservation.finish(13)`. It checks the state is PICKED_UP, moves to RETURNED and says the vehicle was out for 3 days.
  6. The service adds H1 to the pool of the branch named in the call, then asks `PricingPolicy.fee(HATCHBACK, 3, 3)` for the total, which is 3000. If the return had been on day 14, one late day would cost 1500 more.

Patterns used

  • State (as an enum with guarded moves): Reservation moves through RESERVED, PICKED_UP, RETURNED and CANCELLED, and only through its own methods.. Every change of state goes through one place, so a move that makes no sense, such as returning a car that was never picked up, is easy to refuse and easy to test. With four states an enum is enough. Full State classes would be too much.
  • Strategy: PricingPolicy (StandardPricing) is held by CarRentalService.. The rate table, the late fee and the discounts change often. With an interface, a new rule is a new class and the service is untouched (the open/closed principle).
  • Pool per branch: Each Branch keeps its free vehicles by type, sorted by plate.. A vehicle is in exactly one place: a branch pool, or out with a reservation. There is no separate location field that can disagree with the pool. Taking from the pool is one step under the branch lock.
  • Facade: CarRentalService gives clients five calls and hides branches, pools, reservations and pricing.. The caller does not need to know which pool a car came from or how a fee is added up. The order of steps stays in one class.

Key code

Reservation: guarded state changes

enum ReservationStatus { RESERVED, PICKED_UP, RETURNED, CANCELLED }

class Reservation {
    final String id;
    final Vehicle vehicle;
    final Branch pickupBranch;
    final int bookedDays;
    private ReservationStatus status = ReservationStatus.RESERVED;
    private int pickupDay;

    Reservation(String id, Vehicle vehicle, Branch pickupBranch, int bookedDays) {
        this.id = id;
        this.vehicle = vehicle;
        this.pickupBranch = pickupBranch;
        this.bookedDays = bookedDays;
    }

    synchronized boolean pickUp(int day) {
        if (status != ReservationStatus.RESERVED) return false;
        status = ReservationStatus.PICKED_UP;
        pickupDay = day;
        return true;
    }

    synchronized boolean cancel() {
        if (status != ReservationStatus.RESERVED) return false;
        status = ReservationStatus.CANCELLED;
        return true;
    }

    /** Days the vehicle was out (at least 1), or -1 if it is not out or the day is before pickup. */
    synchronized int finish(int day) {
        if (status != ReservationStatus.PICKED_UP || day < pickupDay) return -1;
        status = ReservationStatus.RETURNED;
        return Math.max(1, day - pickupDay);
    }
}

PricingPolicy, the branch pool and giveBack

interface PricingPolicy {
    int fee(VehicleType type, int bookedDays, int actualDays);
}

/** Rate times booked days. Each day beyond the booked days costs one and a half times the rate. */
class StandardPricing implements PricingPolicy {
    @Override
    public int fee(VehicleType type, int bookedDays, int actualDays) {
        int rate = switch (type) {
            case HATCHBACK -> 1000;
            case SEDAN -> 1500;
            case SUV -> 2500;
        };
        int lateDays = Math.max(0, actualDays - bookedDays);
        return rate * bookedDays + lateDays * rate * 3 / 2;
    }
}

class Branch {
    private final Map<VehicleType, TreeMap<String, Vehicle>> free = new EnumMap<>(VehicleType.class);

    synchronized void add(Vehicle v) {
        free.computeIfAbsent(v.type, t -> new TreeMap<>()).put(v.plate, v);
    }

    /** One step under the lock: two customers cannot take the same vehicle. */
    synchronized Vehicle take(VehicleType type) {
        TreeMap<String, Vehicle> pool = free.get(type);
        return pool == null || pool.isEmpty() ? null : pool.pollFirstEntry().getValue();
    }
}

// In CarRentalService:
public int giveBack(String reservationId, String branchName, int day) {
    Reservation r = reservations.get(reservationId);
    if (r == null) return -1;
    int days = r.finish(day);            // checks the state and moves to RETURNED
    if (days < 0) return -1;
    branch(branchName).add(r.vehicle);   // free at the branch where it was returned
    return pricing.fee(r.vehicle.type, r.bookedDays, days);
}

Deep dives

Reservation states and guarded moves

Weak: Boolean flags such as pickedUp and returned on the reservation. Now returned = true with pickedUp = false is possible, and every method has to guess what the flags mean together. A returned reservation can be picked up again, and a cancelled one can be returned.

Good: One ReservationStatus enum, with the service checking the status before each change. A reservation can only be in one state, which removes the impossible flag mixes. But the rules for legal moves are now spread across the service methods.

Great: Put the moves on the reservation: pickUp, cancel and finish. Each one checks the current state, changes it only if the move is legal, and returns false (or -1) otherwise. The service never writes the status itself. All legal moves are in one place and match the state diagram: RESERVED goes to PICKED_UP or CANCELLED, and PICKED_UP goes to RETURNED. Synchronize these methods so two calls cannot both pass the check.

Trade-off: An enum with guarded methods is enough for four states with no different behaviour. If each state needs its own logic, such as different fees in each, move to the State pattern with one class per state. Do not start there.

Fees, late days and early returns

Weak: Work out the fee inside giveBack with an if on the vehicle type and another on lateness. A new rate, a weekend rule or a discount edits the same method, and the fee logic cannot be tested without a whole rental.

Good: A PricingPolicy interface with fee(type, bookedDays, actualDays). StandardPricing is the first implementation. The service passes in the booked days and the days actually used, which it gets from the pickup day and return day. Both days come from the caller, so a test needs no clock.

Great: Split the rule into its two parts inside the policy: the base fee is the rate times the booked days, and the late fee is the days beyond the booked days times the late rate. Decide the edge cases and write them down: a same-day return counts as 1 day, a return before pickup is refused, and an early return costs the same as the booked days. Decorators can then add insurance or a one-way charge by wrapping the policy.

Trade-off: Charging for the booked days on an early return is simple and predictable. Some businesses refund unused days. That is a different policy, so write it as a new class and swap it in.

Availability by branch and returns to another branch

Weak: One list of all vehicles, each with a branch field and an available flag. Every availability question scans the whole list. The flag and the branch can disagree: a vehicle marked available at a branch where it is not.

Good: Each branch keeps its own free vehicles, grouped by type. Availability is a count of that group. Reserving removes a vehicle from the group, and returning adds it to the group of the branch named in the call. A vehicle is in exactly one pool, or in none while it is reserved or out.

Great: Keep each pool sorted, for example in a TreeMap by plate. The rule "lowest plate first" then costs nothing and the same input always gives the same vehicle, which tests need. Make take one synchronized step so two customers cannot get one car. The pickup branch is stored on the reservation, so a cancel puts the vehicle back where it came from.

Trade-off: Pools model only what is free right now. To reserve a car for next Friday, each vehicle needs a calendar of booked day ranges, with the overlap rule from the hotel booking problem. Say that this is the next step and why you did not start with it.

Now add one more feature

  • Now add a one-way charge of 500 when a vehicle is returned to a branch other than the pickup branch. What changes? The reservation already holds the pickup branch. Pass whether the branches differ into the pricing policy, or wrap the policy in a `OneWayCharge` decorator. Branches and pools do not change.
  • Now reserve for future dates. What changes? Give each vehicle a calendar of booked day ranges and use the overlap rule: two ranges clash when each starts before the other ends. The branch pool becomes a search over vehicles that are free for the whole range.
  • Now add insurance and a child seat as extras. What changes? Model each extra as a small class with a price, store the chosen extras on the reservation, and let a decorator on `PricingPolicy` add their cost. The state moves stay the same.
  • Now add a waitlist when no vehicle is free. What changes? Keep a queue per branch and type. When `giveBack` adds a vehicle, a listener offers it to the first customer in the queue. The branch publishes an event and the waitlist subscribes (the observer pattern).
  • Now put a vehicle in maintenance. What changes? Add a MAINTENANCE state for the vehicle outside the free pool. `take` never sees it, so reservations are unaffected, and finishing maintenance adds the vehicle back to a branch pool.

What each level is expected to show

SDE-1: A clean diagram with the service, Branch, Vehicle, VehicleType, Reservation and a status enum. Reserve, pick up and return work, illegal moves are refused, and the fee uses the type and the days. The late fee is in one place and the free vehicles are counted per branch.

SDE-2: Everything at SDE-1, plus pricing behind an interface, guarded state moves on the reservation, an atomic take from a branch pool, and a clear plan for future-date reservations with a calendar and the overlap rule. Answers each extension by naming the classes that change.

Common mistakes

  • Boolean flags such as `pickedUp` and `returned` instead of one status, which allow states that cannot exist.
  • Letting the service write the status directly, so the rules for legal moves are copied into many methods.
  • Writing the fee as an `if` chain inside `giveBack`, so every new price rule edits the same method.
  • Reading the system clock inside the service, so fees cannot be tested.
  • A vehicle list with a branch field and an available flag that can disagree, instead of a pool per branch.
  • Not putting a cancelled vehicle back in a pool, so it is lost from the fleet.
  • Subclasses Hatchback, Sedan and Suv with nothing different in them. An enum with a rate is enough.

Lessons behind this answer