Core classes for a library system, reservation queueing when a book is checked out, and fine calculation as a pluggable strategy.
Published September 23, 2026
class Book {
private final String isbn;
private final String title;
private BookStatus status = BookStatus.AVAILABLE; // AVAILABLE, CHECKED_OUT, RESERVED
}
class Member {
private final String memberId;
private final List<Loan> activeLoans = new ArrayList<>();
}
class Loan {
private final Book book;
private final Member member;
private final Instant checkoutDate;
private final Instant dueDate;
private Instant returnDate; // null until returned
}
class Library {
private final Map<String, Book> catalog;
private final Map<String, Queue<Member>> reservationQueues = new HashMap<>(); // per book ISBN
}
Loan is a deliberate separate class rather than a field on Book — a book can have many loans over its lifetime, and keeping loan history (not just current status) requires a record per checkout, not a single mutable field.
class Library {
Optional<Book> checkout(String isbn, Member member) {
Book book = catalog.get(isbn);
if (book.getStatus() != BookStatus.AVAILABLE) return Optional.empty();
book.setStatus(BookStatus.CHECKED_OUT);
loans.add(new Loan(book, member, Instant.now(), Instant.now().plus(Duration.ofDays(14))));
return Optional.of(book);
}
void reserve(String isbn, Member member) {
reservationQueues.computeIfAbsent(isbn, k -> new LinkedList<>()).add(member); // FIFO — first reservation, first served
}
void returnBook(String isbn) {
Book book = catalog.get(isbn);
Queue<Member> queue = reservationQueues.get(isbn);
if (queue != null && !queue.isEmpty()) {
Member next = queue.poll();
book.setStatus(BookStatus.RESERVED); // held for the next member in line, not immediately AVAILABLE
notifyMemberBookReady(next, book);
} else {
book.setStatus(BookStatus.AVAILABLE);
}
}
}
The RESERVED status (distinct from AVAILABLE) matters: a returned book with a waiting reservation queue shouldn't be checkoutable by a walk-in member ahead of whoever reserved it first — this is the detail that separates a design that merely tracks availability from one that correctly models fairness.
interface FineStrategy { double calculateFine(Loan loan); }
class StandardFineStrategy implements FineStrategy {
private static final double DAILY_RATE = 0.25;
public double calculateFine(Loan loan) {
long overdueDays = Math.max(0, Duration.between(loan.getDueDate(), Instant.now()).toDays());
return overdueDays * DAILY_RATE;
}
}
class CappedFineStrategy implements FineStrategy {
private final FineStrategy delegate;
private final double cap;
public double calculateFine(Loan loan) { return Math.min(delegate.calculateFine(loan), cap); } // Decorator over a Strategy
}
Same Strategy Pattern separation used throughout this course: fine policy can differ per library branch or member type without Library's checkout/return logic ever changing. CappedFineStrategy wrapping another FineStrategy is worth noticing — it's Decorator applied to a Strategy object, showing the two patterns aren't mutually exclusive.
The core classes treat Book as a single object with a status. Real libraries own several copies of popular titles, and that one change reshapes the model:
class Title { // what a member searches for and reserves
final String isbn; final String name; final List<String> authors;
Title(String isbn, String name, List<String> authors) { this.isbn = isbn; this.name = name; this.authors = authors; }
}
enum CopyStatus { AVAILABLE, ON_LOAN, ON_HOLD_SHELF, LOST, UNDER_REPAIR }
class BookCopy { // what is physically lent out
final String barcode; final Title title;
CopyStatus status = CopyStatus.AVAILABLE;
BookCopy(String barcode, Title title) { this.barcode = barcode; this.title = title; }
}
Title: a member wants any copy of the book, not copy #3.BookCopy: the library needs to know exactly which physical item is overdue or lost.The interesting logic is on return: if someone is waiting for the title, the copy goes to the hold shelf for them instead of back on the open shelf.
class LibraryService {
private final Map<String, List<BookCopy>> copiesByIsbn;
private final Map<String, Deque<Reservation>> queueByIsbn = new HashMap<>();
private final LoanPolicy policy; // limits and loan period, by member type
private final Clock clock;
LibraryService(Map<String, List<BookCopy>> copiesByIsbn, LoanPolicy policy, Clock clock) {
this.copiesByIsbn = copiesByIsbn; this.policy = policy; this.clock = clock;
}
synchronized Loan checkout(Member member, String isbn) {
if (member.activeLoans().size() >= policy.maxLoans(member))
throw new LoanLimitExceededException(member.id());
if (member.hasUnpaidFinesAbove(policy.fineBlockThreshold()))
throw new MemberBlockedException(member.id());
BookCopy copy = heldFor(member, isbn) // their reserved copy, if any
.or(() -> firstWithStatus(isbn, CopyStatus.AVAILABLE))
.orElseThrow(() -> new NoCopyAvailableException(isbn));
copy.status = CopyStatus.ON_LOAN;
Instant now = clock.instant();
Loan loan = new Loan(copy, member, now, now.plus(policy.loanPeriod(member)));
member.activeLoans().add(loan);
return loan;
}
synchronized Fine returnCopy(Loan loan) {
loan.markReturned(clock.instant());
loan.member().activeLoans().remove(loan);
Fine fine = policy.fineFor(loan); // FineStrategy from above
Deque<Reservation> queue = queueByIsbn.getOrDefault(loan.copy().title.isbn, new ArrayDeque<>());
Reservation next = queue.pollFirst();
if (next != null) {
loan.copy().status = CopyStatus.ON_HOLD_SHELF;
next.assign(loan.copy(), clock.instant().plus(Duration.ofDays(3))); // pickup deadline
notifier.bookReady(next.member(), loan.copy().title);
} else {
loan.copy().status = CopyStatus.AVAILABLE;
}
return fine;
}
}
Points worth making in an interview:
LoanPolicy (max loans, loan period, blocking thresholds), which can differ for students, staff and premium members. They don't belong in if statements scattered through the service.ON_LOAN → ON_HOLD_SHELF), not only on the member.Clock injection again makes due dates, fines and deadlines testable without waiting.Members search by title, author, subject or ISBN. At machine-coding scale, keep simple in-memory indexes (Map<String, Set<Title>> from each lower-cased author name or title word). At larger scale, delegate to a search engine. The CatalogSearch interface stays the same either way.
Q: What happens if a member never picks up a RESERVED book? A: A real system needs a timeout — after N days in RESERVED status unclaimed, the book reverts to AVAILABLE (or offers to the next person in the reservation queue), which would be implemented as a scheduled check rather than something triggered by a user action.
Q: Why is reservationQueues keyed by ISBN rather than by a specific physical book copy? A: This design assumes reservations are for a title, not a specific physical copy — if a library has multiple copies of the same ISBN, any returned copy can satisfy the next reservation, which is the more realistic and more useful behavior than binding a reservation to one specific physical book.
Q: How would you extend this to support multiple copies of the same book? A: Separate the concept of a Book (the title/metadata) from a BookCopy (one physical, individually trackable instance with its own status) — Book would hold a List<BookCopy>, and checkout logic would search for any AVAILABLE copy rather than checking a single book's status directly.
Q: Why model Title and BookCopy separately?
A: They answer different questions. Members search for and reserve a title, while loans, damage and loss happen to specific physical copies. Merging them makes multiple copies, per-copy history and "any copy available" logic awkward.
Q: Where should rules like "students can borrow 3 books for 14 days" live? A: In a policy object selected by member type (Strategy), not hard-coded in the checkout method. Rules change more often than the checkout flow does, and policies can be tested and configured on their own.
Q: How do you stop two librarians checking out the last copy to two members at once?
A: Make the status change atomic: synchronize within a single process, or use a conditional update (UPDATE copy SET status='ON_LOAN' WHERE barcode=? AND status='AVAILABLE') in a database, and treat 0 updated rows as "no longer available".