Low-level design · Problem P09 of 15 · Classic systems
Library management
The trap in this problem is the first noun: a library has books, but you lend copies. Separating the title from its physical copies, and giving each copy a status, makes loans, holds and fines fall into place.
1. The prompt
The prompt"Design a library system. Members borrow and return books, with a limit on how many they can hold. Late returns are fined. Members can reserve a book that's out."
2. Clarify the requirements
Spend the first five minutes here. Ask, then write the answers down where the interviewer can see them.
| Ask | Assume (unless told otherwise) |
|---|---|
| How long is a loan? | 14 days. |
| Fine? | 25 cents per day late. |
| How many loans per member? | Two. |
| When a reserved book comes back? | It's held for the first member waiting, and nobody else can borrow it. |
Must do
- Borrow a copy of a title if one is free
- Return it; fine if late
- Waitlist: a returned copy is held for the next member in line
- Limit loans per member
Must be
- No copy lent to two members
- Fines and loan rules easy to change
- Fair: first in the waitlist, first served
Out of scope
- Search and catalogue
- Renewals (a follow-up)
- Payments for fines
3. The classes
| Class | Its one job |
|---|---|
| Library | Borrow and return, applying the rules |
| BookCopy | One physical copy and its status: available, on loan, on hold |
| Member | Their loans and limit |
| Loan | Which copy, which member, when it's due |
| FinePolicy | How much a late return costs |
| Waitlist | A queue of members per title |
4. How it works
5. Patterns and why
| Pattern | Where | Why it's worth it here |
|---|---|---|
| Title vs instance | Book (isbn, title) vs BookCopy (id, status) | You lend copies; a title can have many |
| State as an enum | CopyStatus: AVAILABLE, ON_LOAN, ON_HOLD | Checkout rules read the status, not a tangle of flags |
| Strategy | FinePolicy | Students and staff can have different fines without editing Library |
Memory hook: "Lend copies, queue for titles"Status lives on each copy; the waitlist is per title, and a returned copy is held for the head of the queue.
6. The code
Days are plain numbers to keep the demo exact. The member ids and copy ids are strings.
from __future__ import annotations
from collections import deque
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Optional
class CopyStatus(Enum):
AVAILABLE = auto()
ON_LOAN = auto()
ON_HOLD = auto()
@dataclass
class Copy:
id: str
isbn: str
status: CopyStatus = CopyStatus.AVAILABLE
held_for: Optional[str] = None
@dataclass
class Member:
id: str
max_loans: int = 2
loans: set[str] = field(default_factory=set) # copy ids
@dataclass(frozen=True)
class Loan:
copy_id: str
member_id: str
due_day: int
class FinePolicy:
def __init__(self, cents_per_day: int = 25) -> None:
self.rate = cents_per_day
def fine(self, due_day: int, returned_day: int) -> int:
return max(0, returned_day - due_day) * self.rate
class Library:
LOAN_DAYS = 14
def __init__(self, fines: FinePolicy) -> None:
self.fines = fines
self.titles: dict[str, str] = {}
self.copies: dict[str, Copy] = {}
self.members: dict[str, Member] = {}
self.loans: dict[str, Loan] = {} # copy id -> loan
self.waitlist: dict[str, deque[str]] = {} # isbn -> member ids
def add_book(self, isbn: str, title: str, copy_ids: list[str]) -> None:
self.titles[isbn] = title
for cid in copy_ids:
self.copies[cid] = Copy(cid, isbn)
def join(self, member_id: str) -> None:
self.members[member_id] = Member(member_id)
def checkout(self, member_id: str, isbn: str, today: int) -> str:
m, title = self.members[member_id], self.titles[isbn]
if len(m.loans) >= m.max_loans:
return f"{member_id}: loan limit reached"
copy = next((c for c in self.copies.values() if c.isbn == isbn and
(c.status is CopyStatus.AVAILABLE or (c.status is CopyStatus.ON_HOLD and c.held_for == member_id))), None)
if copy is None:
q = self.waitlist.setdefault(isbn, deque())
if member_id not in q:
q.append(member_id)
return f"{member_id}: no copy of {title} free, added to waitlist (position {list(q).index(member_id) + 1})"
copy.status, copy.held_for = CopyStatus.ON_LOAN, None
m.loans.add(copy.id)
self.loans[copy.id] = Loan(copy.id, member_id, today + self.LOAN_DAYS)
return f"{member_id}: borrowed {title} ({copy.id}), due day {today + self.LOAN_DAYS}"
def give_back(self, copy_id: str, today: int) -> str:
loan = self.loans.pop(copy_id)
self.members[loan.member_id].loans.discard(copy_id)
copy = self.copies[copy_id]
fine = self.fines.fine(loan.due_day, today)
msg = f"{loan.member_id}: returned {copy_id}" + (f", fine ${fine // 100}.{fine % 100:02d}" if fine else "")
q = self.waitlist.get(copy.isbn)
if q:
copy.status, copy.held_for = CopyStatus.ON_HOLD, q.popleft()
msg += f"; held for {copy.held_for}"
else:
copy.status = CopyStatus.AVAILABLE
return msg
if __name__ == "__main__":
lib = Library(FinePolicy(25))
lib.add_book("978-1449373320", "Designing Data-Intensive Applications", ["DDIA-1"])
lib.add_book("978-0262510875", "SICP", ["SICP-1", "SICP-2"])
for m in ("ana", "ben", "cara"):
lib.join(m)
print(lib.checkout("ana", "978-1449373320", 1))
print(lib.checkout("ben", "978-1449373320", 2))
print(lib.checkout("ana", "978-0262510875", 3))
print(lib.checkout("ana", "978-0262510875", 3))
print(lib.give_back("DDIA-1", 18))
print(lib.checkout("cara", "978-1449373320", 18))
print(lib.checkout("ben", "978-1449373320", 19))
enum CopyStatus { AVAILABLE, ON_LOAN, ON_HOLD }
class BookCopy {
status = CopyStatus.AVAILABLE;
heldFor: string | null = null;
constructor(readonly id: string, readonly isbn: string) {}
}
class Member {
readonly loans = new Set<string>(); // copy ids
constructor(readonly id: string, readonly maxLoans = 2) {}
}
interface Loan { copyId: string; memberId: string; dueDay: number; }
class FinePolicy {
constructor(private centsPerDay = 25) {}
fine(dueDay: number, returnedDay: number): number { return Math.max(0, returnedDay - dueDay) * this.centsPerDay; }
}
class Library {
static readonly LOAN_DAYS = 14;
private titles = new Map<string, string>();
private copies = new Map<string, BookCopy>();
private members = new Map<string, Member>();
private loans = new Map<string, Loan>(); // copy id -> loan
private waitlist = new Map<string, string[]>(); // isbn -> member ids
constructor(private fines: FinePolicy) {}
addBook(isbn: string, title: string, copyIds: string[]): void {
this.titles.set(isbn, title);
for (const id of copyIds) this.copies.set(id, new BookCopy(id, isbn));
}
join(memberId: string): void { this.members.set(memberId, new Member(memberId)); }
checkout(memberId: string, isbn: string, today: number): string {
const m = this.members.get(memberId)!, title = this.titles.get(isbn)!;
if (m.loans.size >= m.maxLoans) return `${memberId}: loan limit reached`;
const copy = [...this.copies.values()].find((c) => c.isbn === isbn &&
(c.status === CopyStatus.AVAILABLE || (c.status === CopyStatus.ON_HOLD && c.heldFor === memberId)));
if (!copy) {
const q = this.waitlist.get(isbn) ?? [];
if (!q.includes(memberId)) q.push(memberId);
this.waitlist.set(isbn, q);
return `${memberId}: no copy of ${title} free, added to waitlist (position ${q.indexOf(memberId) + 1})`;
}
copy.status = CopyStatus.ON_LOAN;
copy.heldFor = null;
m.loans.add(copy.id);
const due = today + Library.LOAN_DAYS;
this.loans.set(copy.id, { copyId: copy.id, memberId, dueDay: due });
return `${memberId}: borrowed ${title} (${copy.id}), due day ${due}`;
}
giveBack(copyId: string, today: number): string {
const loan = this.loans.get(copyId)!;
this.loans.delete(copyId);
this.members.get(loan.memberId)!.loans.delete(copyId);
const copy = this.copies.get(copyId)!;
const fine = this.fines.fine(loan.dueDay, today);
let msg = `${loan.memberId}: returned ${copyId}` + (fine ? `, fine $${Math.floor(fine / 100)}.${String(fine % 100).padStart(2, "0")}` : "");
const q = this.waitlist.get(copy.isbn);
if (q && q.length) {
copy.status = CopyStatus.ON_HOLD;
copy.heldFor = q.shift()!;
msg += `; held for ${copy.heldFor}`;
} else copy.status = CopyStatus.AVAILABLE;
return msg;
}
}
const lib = new Library(new FinePolicy(25));
lib.addBook("978-1449373320", "Designing Data-Intensive Applications", ["DDIA-1"]);
lib.addBook("978-0262510875", "SICP", ["SICP-1", "SICP-2"]);
for (const m of ["ana", "ben", "cara"]) lib.join(m);
console.log(lib.checkout("ana", "978-1449373320", 1));
console.log(lib.checkout("ben", "978-1449373320", 2));
console.log(lib.checkout("ana", "978-0262510875", 3));
console.log(lib.checkout("ana", "978-0262510875", 3));
console.log(lib.giveBack("DDIA-1", 18));
console.log(lib.checkout("cara", "978-1449373320", 18));
console.log(lib.checkout("ben", "978-1449373320", 19));import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
enum CopyStatus { AVAILABLE, ON_LOAN, ON_HOLD }
class BookCopy {
final String id, isbn;
CopyStatus status = CopyStatus.AVAILABLE;
String heldFor;
BookCopy(String id, String isbn) {
this.id = id;
this.isbn = isbn;
}
}
class Member {
final String id;
final int maxLoans;
final Set<String> loans = new HashSet<>(); // copy ids
Member(String id, int maxLoans) {
this.id = id;
this.maxLoans = maxLoans;
}
}
record Loan(String copyId, String memberId, int dueDay) {}
class FinePolicy {
private final int centsPerDay;
FinePolicy(int centsPerDay) {
this.centsPerDay = centsPerDay;
}
int fine(int dueDay, int returnedDay) {
return Math.max(0, returnedDay - dueDay) * centsPerDay;
}
}
class Library {
static final int LOAN_DAYS = 14;
private final FinePolicy fines;
private final Map<String, String> titles = new HashMap<>();
private final Map<String, BookCopy> copies = new LinkedHashMap<>();
private final Map<String, Member> members = new HashMap<>();
private final Map<String, Loan> loans = new HashMap<>(); // copy id -> loan
private final Map<String, Deque<String>> waitlist = new HashMap<>(); // isbn -> member ids
Library(FinePolicy fines) {
this.fines = fines;
}
void addBook(String isbn, String title, List<String> copyIds) {
titles.put(isbn, title);
copyIds.forEach(id -> copies.put(id, new BookCopy(id, isbn)));
}
void join(String memberId) {
members.put(memberId, new Member(memberId, 2));
}
synchronized String checkout(String memberId, String isbn, int today) {
Member m = members.get(memberId);
String title = titles.get(isbn);
if (m.loans.size() >= m.maxLoans) return memberId + ": loan limit reached";
BookCopy copy = copies.values().stream().filter(c -> c.isbn.equals(isbn) && (c.status == CopyStatus.AVAILABLE
|| (c.status == CopyStatus.ON_HOLD && memberId.equals(c.heldFor)))).findFirst().orElse(null);
if (copy == null) {
Deque<String> q = waitlist.computeIfAbsent(isbn, k -> new ArrayDeque<>());
if (!q.contains(memberId)) q.addLast(memberId);
return memberId + ": no copy of " + title + " free, added to waitlist (position " + (List.copyOf(q).indexOf(memberId) + 1) + ")";
}
copy.status = CopyStatus.ON_LOAN;
copy.heldFor = null;
m.loans.add(copy.id);
loans.put(copy.id, new Loan(copy.id, memberId, today + LOAN_DAYS));
return memberId + ": borrowed " + title + " (" + copy.id + "), due day " + (today + LOAN_DAYS);
}
synchronized String giveBack(String copyId, int today) {
Loan loan = loans.remove(copyId);
members.get(loan.memberId()).loans.remove(copyId);
BookCopy copy = copies.get(copyId);
int fine = fines.fine(loan.dueDay(), today);
StringBuilder msg = new StringBuilder(loan.memberId() + ": returned " + copyId);
if (fine > 0) msg.append(", fine $").append(fine / 100).append(".").append(fine % 100 < 10 ? "0" : "").append(fine % 100);
Deque<String> q = waitlist.get(copy.isbn);
if (q != null && !q.isEmpty()) {
copy.status = CopyStatus.ON_HOLD;
copy.heldFor = q.pollFirst();
msg.append("; held for ").append(copy.heldFor);
} else copy.status = CopyStatus.AVAILABLE;
return msg.toString();
}
}
public class Main {
public static void main(String[] args) {
Library lib = new Library(new FinePolicy(25));
lib.addBook("978-1449373320", "Designing Data-Intensive Applications", List.of("DDIA-1"));
lib.addBook("978-0262510875", "SICP", List.of("SICP-1", "SICP-2"));
for (String m : List.of("ana", "ben", "cara")) lib.join(m);
System.out.println(lib.checkout("ana", "978-1449373320", 1));
System.out.println(lib.checkout("ben", "978-1449373320", 2));
System.out.println(lib.checkout("ana", "978-0262510875", 3));
System.out.println(lib.checkout("ana", "978-0262510875", 3));
System.out.println(lib.giveBack("DDIA-1", 18));
System.out.println(lib.checkout("cara", "978-1449373320", 18));
System.out.println(lib.checkout("ben", "978-1449373320", 19));
}
}Output (the same in all three languages)
ana: borrowed Designing Data-Intensive Applications (DDIA-1), due day 15 ben: no copy of Designing Data-Intensive Applications free, added to waitlist (position 1) ana: borrowed SICP (SICP-1), due day 17 ana: loan limit reached ana: returned DDIA-1, fine $0.75; held for ben cara: no copy of Designing Data-Intensive Applications free, added to waitlist (position 1) ben: borrowed Designing Data-Intensive Applications (DDIA-1), due day 33
7. Concurrency and edge cases
- Two members, one free copy: checking availability and marking the copy on loan must happen together (the Java version synchronizes; a database would use a conditional update).
- Holds that are never collected should expire after a few days and pass to the next person in the queue.
- Returning a copy twice or an unknown copy must be rejected.
8. Follow-up questions
What interviewers ask once the core works, and a short answer for each.
- How would you add renewals?
- Allowed if nobody is on the waitlist for that title and the renewal limit isn't reached; push the due day out.
- How would you send due-date reminders?
- A daily job finds loans due tomorrow and publishes events; a notifier subscribes (observer).
- Search by author or title?
- A separate catalogue/search component with an index; keep it out of the lending rules.
9. Common mistakes
One Book class with an available flagThen a title can't have several copies, or each copy duplicates the title data.
Putting a returned reserved book back on the shelfSomeone else can grab it before the person who waited.
Fines computed in five placesPut the rule in one policy object.
10. Check yourself
Why separate Book from BookCopy?
Status belongs to a copy; title data belongs to the book.
A reserved book is returned. What happens?
Fairness for the people who waited.
Your turn
- Add renewals (10 min). Only when nobody is waiting.
- Expire uncollected holds (10 min). After three days, pass the copy to the next member.