Courses › Low-Level Design › Problem Walkthroughs

Design a Library & Car Rental System

Title vs copy, loans, holds and fines, and date-range availability for reservations.

"Design a library management system" and "design a car rental system" look different, but they're the same problem underneath: lend physical units of a catalog item to members for a period of time, with availability, holds or reservations, returns and late fees. Learn the shared model once and you can handle both, along with equipment rental, hotel rooms and bike sharing.

The key modeling move: title vs. copy

The most common mistake is a single Book class. A library has a title ("Dune", with an ISBN and an author) and several copies of it, each with its own barcode, condition and status. A car rental company has a car class ("compact SUV") and individual vehicles with license plates and mileage.

Members reserve catalog items, not units: you put a hold on "Dune", not on copy #3, and you reserve "a compact car", not a specific plate. A specific unit is assigned only when it's picked up.

Library: requirements

  • Search titles; see how many copies are available.
  • Borrow a copy if one is available and the member is under their limit and has no unpaid fines.
  • Return a copy; charge a fine per day late.
  • If no copy is free, place a hold. When a copy comes back, it's reserved for the first member in that title's hold queue for a few days.

Library: implementation

from collections import deque
from datetime import date, timedelta
from enum import Enum


class CopyStatus(Enum):
    AVAILABLE = "available"
    ON_LOAN = "on_loan"
    ON_HOLD_SHELF = "on_hold_shelf"


class Title:
    def __init__(self, isbn, name):
        self.isbn, self.name = isbn, name
        self.copies = []
        self.holds = deque()               # members waiting, first come first served


class Copy:
    def __init__(self, barcode, title):
        self.barcode, self.title = barcode, title
        self.status = CopyStatus.AVAILABLE
        self.held_for = None
        title.copies.append(self)


class Member:
    def __init__(self, member_id, limit=3):
        self.id, self.limit = member_id, limit
        self.loans = {}                    # barcode -> Loan
        self.fines_due = 0


class Loan:
    def __init__(self, copy, member, due):
        self.copy, self.member, self.due = copy, member, due


class LibraryError(Exception): ...


class Library:
    LOAN_DAYS, FINE_PER_DAY = 14, 25       # fine in cents

    def __init__(self):
        self.titles = {}

    def add_title(self, title):
        self.titles[title.isbn] = title

    def borrow(self, member, isbn, today):
        if member.fines_due > 0:
            raise LibraryError("unpaid fines")
        if len(member.loans) >= member.limit:
            raise LibraryError("borrowing limit reached")
        title = self.titles[isbn]
        copy = next((c for c in title.copies if c.status is CopyStatus.ON_HOLD_SHELF
                     and c.held_for is member), None)
        if copy is None:
            copy = next((c for c in title.copies if c.status is CopyStatus.AVAILABLE), None)
        if copy is None:
            raise LibraryError("no copy available; place a hold")
        copy.status, copy.held_for = CopyStatus.ON_LOAN, None
        loan = Loan(copy, member, today + timedelta(days=self.LOAN_DAYS))
        member.loans[copy.barcode] = loan
        return loan

    def place_hold(self, member, isbn):
        title = self.titles[isbn]
        if member in title.holds:
            raise LibraryError("already on the hold list")
        title.holds.append(member)

    def return_copy(self, member, barcode, today):
        loan = member.loans.pop(barcode)
        late_days = max(0, (today - loan.due).days)
        member.fines_due += late_days * self.FINE_PER_DAY
        copy = loan.copy
        title = copy.title
        if title.holds:                    # the copy goes to the next waiting member
            copy.status, copy.held_for = CopyStatus.ON_HOLD_SHELF, title.holds.popleft()
        else:
            copy.status = CopyStatus.AVAILABLE
        return late_days
lib = Library()
dune = Title("978-0441", "Dune")
lib.add_title(dune)
Copy("C1", dune)
ana, bo = Member("ana"), Member("bo")

d0 = date(2026, 3, 1)
lib.borrow(ana, "978-0441", d0)
try:
    lib.borrow(bo, "978-0441", d0)
except LibraryError as e:
    print(e)                                         # no copy available; place a hold
lib.place_hold(bo, "978-0441")

late = lib.return_copy(ana, "C1", d0 + timedelta(days=17))
print(late, ana.fines_due)                           # 3 75
print(dune.copies[0].status, dune.copies[0].held_for.id)   # CopyStatus.ON_HOLD_SHELF bo
print(lib.borrow(bo, "978-0441", d0 + timedelta(days=18)).due)   # 2026-04-02

Car rental: availability over time

The library only cares about *now*. Car rental reservations are for future date ranges, so availability means "for this car class at this branch, are there enough vehicles that aren't booked during any overlapping period?". Two ranges a1, a2) and [b1, b2) overlap exactly when a1 < b2 and b1 < a2 (see [Intervals).

class RentalError(Exception): ...


class Branch:
    def __init__(self, fleet):
        self.fleet = fleet                     # car class -> number of vehicles
        self.reservations = []                 # (car_class, start, end, customer)

    def available(self, car_class, start, end):
        # Peak number of overlapping reservations during [start, end), by a sweep
        events = []
        for cls, s, e, _ in self.reservations:
            if cls == car_class and s < end and start < e:
                events += [(max(s, start), 1), (min(e, end), -1)]
        peak = current = 0
        for _, delta in sorted(events, key=lambda ev: (ev[0], ev[1])):   # ends before starts
            current += delta
            peak = max(peak, current)
        return self.fleet.get(car_class, 0) - peak

    def reserve(self, customer, car_class, start, end):
        if not start < end:
            raise RentalError("invalid dates")
        if self.available(car_class, start, end) <= 0:
            raise RentalError("no vehicles of that class for those dates")
        self.reservations.append((car_class, start, end, customer))


b = Branch({"compact": 2})
b.reserve("ana", "compact", date(2026, 5, 1), date(2026, 5, 4))
b.reserve("bo", "compact", date(2026, 5, 3), date(2026, 5, 6))
print(b.available("compact", date(2026, 5, 2), date(2026, 5, 5)))   # 0
print(b.available("compact", date(2026, 5, 4), date(2026, 5, 8)))   # 1 (ana returns on the 4th)
try:
    b.reserve("cy", "compact", date(2026, 5, 3), date(2026, 5, 4))
except RentalError as e:
    print(e)

Counting overlapping reservations is not enough: two reservations that don't overlap *each other* can share one car. The sweep finds the peak number of cars needed at once during the requested period, which is what matters.

Design decisions to explain

  • Title vs. copy, class vs. vehicle. Holds and reservations attach to the catalog item; loans and rentals attach to the unit.
  • Status as an enum on the unit, changed only by Library methods, so a copy can't be both on loan and on the hold shelf.
  • Holds are a FIFO queue per title, and a returned copy goes straight to the hold shelf for the next member, rather than back to general availability.
  • Fines are computed on return from the due date, so there's no daily background job.
  • Half-open date ranges [start, end) make back-to-back bookings work without off-by-one errors.
  • Dates are passed in, which keeps tests deterministic.

[!INTERVIEW] Scope it early Both prompts can grow without limit: payments, search, recommendations, damage reports, loyalty points. In the first five minutes, agree on three or four core use cases (borrow, return, hold, fines; or search, reserve, pick up, return) and mention the rest as extensions.

Follow-ups

  • Hold expiry: if the member doesn't collect a held copy within a few days, pass it to the next member. Store a hold_expires date and check it on each operation, or with a daily job.
  • Assigning a vehicle at pickup: choose a free vehicle of the class at the branch, preferring the lowest mileage or the one that's been idle longest (a strategy).
  • One-way rentals: the car ends up at another branch, which changes that branch's future availability.
  • Pricing: daily rates by class and season, weekend discounts, insurance and fuel options. Use composable pricing rules, as in Handling Extension Follow-Ups.
  • Concurrency: two customers reserving the last car at the same moment. Hold a lock per branch and car class around "check availability, then reserve", or use a database constraint.
  • Notifications: tell the member when their hold is ready (Observer; see Design a Notification Service).

[!TIP] Practice Library Management System · Car Rental System · Design Movie Ticket Booking · Class Modeling