Skip to main content

Library Management

Design the desk software for a library: members check books out, return them, put a hold on a copy that's out, and get charged if they're late. It's a small domain, which is exactly why it's a good interview prompt - there's nowhere to hide a sloppy model behind sheer size, and the fee math has a habit of leaking into three classes if you let it.

Requirements

Functional

  • A Catalog maps a Book (title, author, ISBN) to one or more physical Copys.
  • A Member checks out an available copy, creating a Loan; returning it closes the loan and frees the copy.
  • If every copy of a book is checked out, a member can place a Hold; the next return of that book should go to the earliest hold in line, not back onto the open shelf.
  • Overdue loans accrue a fee, calculated per day late.

Non-functional

  • The overdue fee formula (flat per-day today, tiered or capped tomorrow) must not require editing Loan or Member.
  • Checking whether a book is available, and who is next in line for it, must not require scanning every loan or hold in the system.

Design

Copy is the unit that actually moves between "on the shelf" and "checked out" - Book is just catalog metadata shared by every copy of it. Fee math is pulled into a FeePolicy the same way FeeStrategy handles pricing in Parking Lot: Loan asks it for a number and never computes one itself.

MemberCatalogBookLoancheckout(isbn, member)1findAvailableCopy()2new Loan(copy, member)3returnBook()4resolveReturn(copy)5
  1. 1The member never touches a Copy directly - the catalog is the only class that finds one.
  2. 2The book (not the catalog) knows which of its own copies are on the shelf.
  3. 3A loan is created for the specific copy found - not for "a copy of this book" in the abstract.
  4. 4Returning goes through the loan itself, since it is the one record of who has this exact copy.
  5. 5The book decides whether the returned copy satisfies its own hold queue or goes back on the shelf.

Each Book keeps its own hold queue, so "who's next" is a peek at that book's queue, and returning a copy either satisfies the head of that queue or puts the copy back on the shelf - never both.

Class diagram

«interface»FeePolicy+ calculateFee(daysLate): doubleCatalog- booksByIsbn: Map<string, Book>+ checkout(isbn, member): Loan+ placeHold(isbn, member): HoldBook- isbn: string- title: string- copies: List<Copy>- holdQueue: Queue<Hold>+ findAvailableCopy(): Copy+ resolveReturn(copy)Copy- id: string- status: CopyStatusLoan- copy: Copy- member: Member- dueDate: date+ returnBook(): doubleHold- member: Member- placedAt: dateMember- id: string- name: string
usescreates
Catalog groups Copies under a Book; Loan asks FeePolicy for the overdue charge; each Book owns its own hold queue.

Code

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.*;
 
enum CopyStatus { AVAILABLE, CHECKED_OUT, ON_HOLD }
 
class Member {
final String id;
final String name;
 
Member(String id, String name) {
this.id = id;
this.name = name;
}
}
 
class Copy {
final String id;
CopyStatus status = CopyStatus.AVAILABLE;
 
Copy(String id) {
this.id = id;
}
}
 
class Hold {
final Member member;
final LocalDate placedAt;
 
Hold(Member member) {
this.member = member;
this.placedAt = LocalDate.now();
}
}
 
interface FeePolicy {
double calculateFee(long daysLate);
}
 
class FlatPerDayFeePolicy implements FeePolicy {
private final double ratePerDay;
 
FlatPerDayFeePolicy(double ratePerDay) {
this.ratePerDay = ratePerDay;
}
 
public double calculateFee(long daysLate) {
return daysLate <= 0 ? 0.0 : daysLate * ratePerDay;
}
}
 
class Loan {
final Copy copy;
final Member member;
final LocalDate dueDate;
private final Book book;
private final FeePolicy feePolicy;
 
Loan(Copy copy, Member member, Book book, LocalDate dueDate, FeePolicy feePolicy) {
this.copy = copy;
this.member = member;
this.book = book;
this.dueDate = dueDate;
this.feePolicy = feePolicy;
copy.status = CopyStatus.CHECKED_OUT;
}
 
double returnBook() {
long daysLate = ChronoUnit.DAYS.between(dueDate, LocalDate.now());
book.resolveReturn(copy);
return feePolicy.calculateFee(daysLate);
}
}
 
class Book {
final String isbn;
final String title;
private final List<Copy> copies;
private final Queue<Hold> holdQueue = new LinkedList<>();
 
Book(String isbn, String title, List<Copy> copies) {
this.isbn = isbn;
this.title = title;
this.copies = copies;
}
 
Optional<Copy> findAvailableCopy() {
return copies.stream().filter(c -> c.status == CopyStatus.AVAILABLE).findFirst();
}
 
void placeHold(Member member) {
holdQueue.add(new Hold(member));
}
 
void resolveReturn(Copy copy) {
Hold nextHold = holdQueue.poll();
if (nextHold != null) {
copy.status = CopyStatus.ON_HOLD;
} else {
copy.status = CopyStatus.AVAILABLE;
}
}
}
 
class Catalog {
private final Map<String, Book> booksByIsbn = new HashMap<>();
private final FeePolicy feePolicy;
 
Catalog(FeePolicy feePolicy) {
this.feePolicy = feePolicy;
}
 
void addBook(Book book) {
booksByIsbn.put(book.isbn, book);
}
 
Loan checkout(String isbn, Member member) {
Book book = booksByIsbn.get(isbn);
Copy copy = book.findAvailableCopy()
.orElseThrow(() -> new IllegalStateException("No copies available for " + isbn));
return new Loan(copy, member, book, LocalDate.now().plusDays(14), feePolicy);
}
 
void placeHold(String isbn, Member member) {
booksByIsbn.get(isbn).placeHold(member);
}
}

Design decisions

  • Book and Copy are separate classes, not one class with a copiesAvailable counter. A counter can tell you how many copies are free, but not which one - and a hold needs to be satisfied by a specific physical copy being returned, not by a number ticking up. Splitting them means a Loan and a Hold can each point at the exact copy they care about.
  • Overdue fee calculation is a FeePolicy, resolved once per Loan.returnBook() call. Same reasoning as the parking lot's FeeStrategy: pricing is the part of a library that changes when policy changes (a fee cap, a grace period, different rates for reference vs. lending copies), so isolating it means Loan never edits when the fee schedule does.
  • The hold queue lives on Book, not on a global HoldManager. A hold is always "next in line for this book," so keeping the queue where the book already lives makes "who gets this copy next" a local check on return, instead of a query that has to filter a system-wide hold table by book first.
  • What's missing for a real system: hold expiration (a member who doesn't pick up within 48 hours loses their place) needs a scheduled sweep this design doesn't include, and multi-branch libraries need Copy to carry a branch and holds to express a pickup-location preference - both left out to keep checkout/return/hold the whole focus.
0%0 of 122 pages studied