"Design a library management system. Members can search the catalog, check out and return items, place holds on items that are currently unavailable, and are charged a fine for overdue returns."
B. Difficulty
Intermediate. Every individual operation is simple; the risk is entirely in ending up with one Library god-class handling catalog search, checkout, holds, and fines all inline, instead of a clean split along genuinely different concerns.
C. What This Problem Tests
The Book/BookItem distinction — catalog metadata (a title) versus a specific lendable physical copy (a barcode with its own status) — and why collapsing them causes real bugs
Fine calculation as a Strategy that varies by member type and item category, not a hardcoded formula
Observer for hold notifications — decoupling "a copy became available" from "who is waiting for it"
The same check-then-act race, now on "checking out the last available copy of a title"
D. Requirements
Functional
Search the catalog by title, author, or category
Check out and return an item, tracking due dates
Place a hold on an item with zero available copies; notify the next member in line when a copy is returned
Calculate an overdue fine based on member type and item category
Support multiple physical copies of the same title
Non-Functional
Two members checking out what they each believe is "the last available copy" of a title must not both succeed
Adding a new item category (e.g. DVDs alongside books) must not require changes to checkout/return/hold logic
E. Clarifying Questions
#
Question
Why It Matters
Design Decision Affected
1
Does the fine formula vary by member type (student/faculty/regular) and/or item category?
Determines whether fine calculation needs to be a Strategy or can be one constant rate
Design of FineStrategy
2
How many physical copies can one title have, and are they distinguishable (different condition/location)?
Confirms the Book/BookItem split is necessary rather than one merged entity
Presence of BookItem as distinct from Book
3
Are holds served strictly FIFO, or can priority (e.g. faculty) jump the queue?
Affects the hold-queue data structure and notification order
Design of HoldQueue
4
Should overdue items eventually be treated as lost, with a replacement charge?
Adds a lifecycle state beyond simple checked-out/returned
Scope of BookItem status enum
F. Assumptions
Confirmed: catalog search, checkout/return, holds with notification, member-type-and-category-aware fines, multiple copies per title.
Assumption: holds are served strictly FIFO per title unless asked otherwise.
Assumption: item categories beyond books (DVDs, magazines) are in scope structurally, via a small type hierarchy, even if books are the primary worked example.
Top-level API coordinating catalog, checkout, and holds
H. Core Domain Entities
Class
Responsibility
Key Attributes
Key Methods
Does NOT Handle
Book (catalog metadata)
Title-level information shared by every physical copy
title, author, ISBN, category
—
Anything about a specific copy's status or location
BookItem (physical copy)
One lendable copy and its own lifecycle status
barcode, status, dueDate
checkout(), returnItem()
Title-level metadata — delegates to its Book reference
Member
A patron and their active loans/holds
id, membershipType
—
Fine calculation logic itself
Loan
One active checkout record
member, bookItem, dueDate
isOverdue()
—
Hold
One member's place in a title's wait queue
member, book, timestamp
—
—
HoldQueue
FIFO queue of holds per title, and the Observer subject for availability
—
addHold(), notifyNext(bookItem)
Deciding checkout eligibility itself
FineStrategy (interface)
One fine-calculation rule
—
calculateFine(loan)
—
Catalog
Search index over Book entries
—
search(query)
Copy-level availability — a search result still needs a live BookItem lookup
Library (facade)
Top-level orchestration
catalog, holdQueues
checkout(), returnItem(), placeHold()
—
I. Class Relationships
Book vs. BookItem is the central modeling decision: a title's metadata (author, ISBN) is shared and immutable across every physical copy, while a copy's status (available, checked out, on hold, lost) is per-instance and constantly changing. Merging them into one entity is the single most common mistake in this problem — it either duplicates title metadata across every copy row or makes "how many copies of this title exist" an awkward query instead of a natural List<BookItem>.
Observer for holds: a HoldQueue is the subject; when a BookItem.returnItem() transitions a copy back to available, it notifies the HoldQueue for that title, which pops the next Hold in FIFO order and notifies that member — the same decouple-the-trigger-from-the-reaction shape as Ride-Sharing's and Notification System's hooks, at a comparably small scope (not the foundational architecture Pub-Sub generalized it into, just another instance of the hook use).
Strategy for fines: a fine depends on both member type and item category, and libraries change these rates far more often than they change checkout logic — isolating this behind FineStrategy.calculateFine(loan) means a rate change is a configuration/strategy swap, not a code change to Loan or Library.
Factory for catalog item types: constructing the right Book/DVD/Magazine subtype (each with its own default loan duration) from raw catalog-ingest data is a small Factory, keeping Catalog itself ignorant of which concrete type it's holding.
J. Class Diagram
View Mermaid source
classDiagram
class Book {
title
author
isbn
category
}
class BookItem {
barcode
status
dueDate
+checkout()
+returnItem()
}
class Loan {
dueDate
+isOverdue()
}
class HoldQueue {
+addHold()
+notifyNext(bookItem)
}
class FineStrategy {
<<interface>>
+calculateFine(loan)
}
class Library {
+checkout()
+returnItem()
+placeHold()
}
Book "1" --> "*" BookItem
BookItem --> Loan
HoldQueue --> "*" Hold
Library --> HoldQueue
Library --> FineStrategy
K. Responsibility Mapping
Class
Responsibility
Does NOT Handle
Book
Title-level metadata
Copy-level status
BookItem
One copy's lifecycle
Title metadata, fine amount
HoldQueue
FIFO wait order and availability notification per title
Checkout eligibility decisions
FineStrategy implementations
One fine formula
Loan record-keeping itself
Library
Top-level orchestration
Catalog search internals, fine math
L. SOLID Analysis
Principle
How It's Applied
Naive Violation
SRP
Book owns metadata; BookItem owns copy status; HoldQueue owns wait order; FineStrategy owns fine math
One Library god-class with methods for search, checkout, holds, and fines all touching the same giant record type
OCP
A new item category is a new Book subtype via Factory; a new fine rule is a new FineStrategy
A fine-calculation if/else chain keyed on member type, edited every time a new membership tier is added
LSP
Every Book subtype (DVD, Magazine) honestly reports its own default loan duration without special-casing by callers
Checkout logic doing if (item instanceof DVD) loanDays = 7 inline instead of asking the item itself
ISP
FineStrategy stays a single-method interface
A combined "LoanPolicy" interface mixing fine calculation with loan-duration and renewal-limit rules that don't always vary together
DIP
Library depends on the FineStrategy abstraction, not a hardcoded rate table
Loan.calculateFine() embedding the rate table directly on the entity itself
M. Design Patterns
Pattern
Problem It Solves
Where It Appears
When NOT to Use
Observer
Availability notification shouldn't require the returning code path to know who's waiting
HoldQueue
Skip if holds are out of scope entirely
Strategy
Fine rates vary by member type and category and change more often than the rest of the system
FineStrategy
Skip if the round only ever needs one flat fine rate
Factory Method
Constructing the correct catalog item subtype from raw ingest data
CatalogItemFactory
Skip if the catalog only ever holds one item type (books only, no DVDs/magazines)
Explicitly avoided: merging Book and BookItem into one entity — see I for why this is the most common and most damaging shortcut in this specific problem.
Ownership/validation notes:checkout() locates a specific BookItem with status AVAILABLE for the requested title, atomically transitions it to CHECKED_OUT, and creates the Loan — it never operates on the Book title level directly, since "available" is a copy-level fact.
Q. Pseudocode — Critical Logic
Library.checkout(member, barcode):
lock(bookItemFor(barcode))
try:
item = bookItemFor(barcode)
if item.status != AVAILABLE: reject
item.status = CHECKED_OUT
loan = new Loan(member, item, dueDate = now + loanPeriod(item.book.category))
return loan
finally:
unlock(item)
BookItem.returnItem():
this.status = AVAILABLE
holdQueue = holdQueueFor(this.book)
if holdQueue.hasWaiting():
nextHold = holdQueue.popNext()
this.status = ON_HOLD // reserved for the notified member, not generally available
notify(nextHold.member, this)
Loan checkout(Member member, String barcode) {
BookItem item = bookItemFor(barcode);
lock(item);
try {
item = bookItemFor(barcode);
if (item.getStatus() != Status.AVAILABLE) return null; // reject
item.setStatus(Status.CHECKED_OUT);
Loan loan = new Loan(member, item, now().plus(loanPeriod(item.getBook().getCategory())));
return loan;
} finally {
unlock(item);
}
}
void returnItem() {
this.status = Status.AVAILABLE;
HoldQueue holdQueue = holdQueueFor(this.book);
if (holdQueue.hasWaiting()) {
Hold nextHold = holdQueue.popNext();
this.status = Status.ON_HOLD; // reserved for the notified member, not generally available
notify(nextHold.getMember(), this);
}
}
std::shared_ptr<Loan> Library::checkout(std::shared_ptr<Member> member, const std::string& barcode) {
auto item = bookItemFor(barcode);
lock(item);
try {
item = bookItemFor(barcode);
if (item->status != Status::AVAILABLE) return nullptr; // reject
item->status = Status::CHECKED_OUT;
auto loan = std::make_shared<Loan>(member, item, now() + loanPeriod(item->book->category));
unlock(item);
return loan;
} catch (...) {
unlock(item);
throw;
}
}
void BookItem::returnItem() {
this->status = Status::AVAILABLE;
auto holdQueue = holdQueueFor(this->book);
if (holdQueue->hasWaiting()) {
auto nextHold = holdQueue->popNext();
this->status = Status::ON_HOLD; // reserved for the notified member, not generally available
notify(nextHold->member, shared_from_this());
}
}
checkout(member, barcode) {
let item = this.bookItemFor(barcode);
lock(item);
try {
item = this.bookItemFor(barcode);
if (item.status !== Status.AVAILABLE) return null; // reject
item.status = Status.CHECKED_OUT;
const loan = new Loan(member, item, addDuration(now(), loanPeriod(item.book.category)));
return loan;
} finally {
unlock(item);
}
}
returnItem() {
this.status = Status.AVAILABLE;
const holdQueue = holdQueueFor(this.book);
if (holdQueue.hasWaiting()) {
const nextHold = holdQueue.popNext();
this.status = Status.ON_HOLD; // reserved for the notified member, not generally available
notify(nextHold.member, this);
}
}
checkout(member: Member, barcode: string): Loan | null {
let item = this.bookItemFor(barcode);
lock(item);
try {
item = this.bookItemFor(barcode);
if (item.status !== Status.AVAILABLE) return null; // reject
item.status = Status.CHECKED_OUT;
const loan = new Loan(member, item, addDuration(now(), loanPeriod(item.book.category)));
return loan;
} finally {
unlock(item);
}
}
returnItem(): void {
this.status = Status.AVAILABLE;
const holdQueue = holdQueueFor(this.book);
if (holdQueue.hasWaiting()) {
const nextHold = holdQueue.popNext();
this.status = Status.ON_HOLD; // reserved for the notified member, not generally available
notify(nextHold.member, this);
}
}
R. End-to-End Use Case
placeHold(memberB, "Clean Code") — no copies available, joins HoldQueue
│
▼
memberA.returnItem(barcode)
│ BookItem.status → AVAILABLE
│ HoldQueue for "Clean Code" has memberB waiting → status → ON_HOLD, memberB notified
▼
memberB.checkout(barcode) within the hold window → status → CHECKED_OUT, Loan created
S. Data Structures
Structure
Used For
Why
Alternative & Trade-off
FIFO queue per title in HoldQueue
Hold ordering
Matches the fairness expectation of "first to ask, first served"
A priority queue if member-type-based priority holds are required (see N)
Inverted index (term → book IDs) in Catalog
Search by title/author/category
Sub-linear lookup instead of scanning every book on every search
A linear scan is fine for a small catalog but doesn't hold up at real library scale
T. Complexity Analysis
Operation
Time
Space
checkout() / returnItem()
O(1) given the copy's barcode
O(1)
search() (indexed)
O(matching terms)
O(index size)
Hold enqueue/notify
O(1) amortized (queue push/pop)
O(waiting members)
U. Testing Strategy
Happy path: checking out the sole available copy of a title makes the title show zero available copies without affecting other titles.
Edge cases: placing a hold when copies are actually available is rejected (holds are for exhausted availability, not a bypass around normal checkout).
Fine tests: the same overdue duration produces different fines for a student vs. a faculty member, and for a book vs. a DVD, without changing any code — only the FineStrategy configuration.
Concurrency tests: two members simultaneously attempting to check out the last available copy of a title result in exactly one success.
V. Interviewer Follow-Ups
Follow-Up
Testing
Design Change
Classes Changed
Pattern/Trade-off
"Add DVDs and magazines with different loan periods."
Whether Book/BookItem was ever merged
New Book subtypes with their own default loan duration
New subclasses + Factory
Validates the Book/BookItem split from I
"Faculty holds should jump ahead of student holds."
Whether HoldQueue ordering was hardcoded FIFO logic scattered elsewhere
Swap the queue's internal ordering to priority-based
HoldQueue only
Validates that ordering was encapsulated, not leaked
"What if a book is reported lost?"
Lifecycle completeness
New LOST status, replacement-cost charge via a dedicated strategy
BookItem, a new fine/charge type
Tests whether the status enum anticipated more than just available/checked-out
W. Concurrency
Shared mutable state: each BookItem's status field, particularly for the last remaining available copy of a popular title.
The check-then-act race, once more: reading "is this copy available" and then setting it to checked-out are two steps; without a lock around the specific BookItem, two members racing for the last copy could both read AVAILABLE before either commits the transition. The fix is the same as everywhere else this bug shape has appeared: lock the specific resource, re-verify, then commit.
Per-copy locking, not per-title: only the one contested BookItem needs locking — checkouts of other copies of the same title, or copies of other titles, proceed independently.
X. Persistence
Catalog entries, loans, holds, and fines are all durable records by nature — a library system loses all value if a checkout isn't remembered across a restart. Each is a natural repository-backed entity; none of this changes the in-memory class design, only where Library's operations ultimately read/write.
Y. Scaling Boundary
Layer
What Belongs Here
LLD
Book/BookItem model, hold queue, fine strategy, per-copy locking
Cross-branch/inter-library systems unless the round pivots there
Z. Failure Scenarios
A notified hold member never comes to collect the reserved copy: the ON_HOLD status should carry an expiry; on expiry, the copy reverts to AVAILABLE (or is offered to the next hold in queue) rather than being reserved indefinitely.
A returned item was never actually checked out (data inconsistency):returnItem() should validate the copy's current status is CHECKED_OUT before transitioning, rejecting a return on an already-available copy rather than silently accepting it.
AA. Common Bad Designs
Merging Book and BookItem into one entity — the defining mistake for this problem; see I.
A single Library class directly implementing search, checkout, holds, and fines — the classic god-object shape this document warns against in nearly every problem, especially damaging here because each of those four concerns genuinely has an independent reason to change.
Hardcoding fine rates as constants inside Loan instead of a swappable FineStrategy — breaks the first time two member types need different rates.
Notifying all waiting members when a copy returns instead of just the next one in FIFO order — technically an Observer pattern, but the wrong fan-out semantics for a single-copy resource.
AB. Design Trade-Offs
Decision
Option A
Option B
Recommendation
Hold expiry handling
Automatic revert to AVAILABLE after a fixed window
Manual librarian intervention
Automatic — a fixed, published hold-pickup window is standard real-library practice and avoids indefinitely locking a copy for a no-show
Catalog search
Inverted index
Linear scan
Inverted index once catalog size is realistic; linear scan is a reasonable V1 for a small demo
AC. Interviewer Evaluation
Strong signal: splitting Book and BookItem unprompted; isolating fine calculation behind a Strategy before being asked for a second member type; catching the last-copy checkout race without being walked into it.
Red flag: one merged Book/copy entity; a god-object Library class; hardcoded fine constants; no expiry on holds.
AD. Interview Simulation
Interviewer: Design a library management system.
Candidate: I'd split a title's catalog metadata — Book — from a specific physical copy — BookItem — since a library can hold multiple copies of the same title, each with its own availability status.
Interviewer: What happens when a member wants a book that's fully checked out?
Candidate: They place a hold, which joins a FIFO queue for that title. When any copy is returned, that copy's return path notifies the queue, which pops the next waiting member and reserves the copy for them — an Observer relationship, so the return code doesn't need to know who, if anyone, is waiting.
Interviewer: How do fines work if students and faculty have different rates?
Candidate: I'd isolate fine calculation behind a FineStrategy keyed by member type and item category, rather than hardcoding rates on the loan itself — new membership tiers become a configuration change, not a code change.
Interviewer: Two members try to check out the last copy of a popular title at the same instant.
Candidate: Same fix as everywhere else this comes up — lock that specific copy, re-check its status is still available, then commit the checkout. Whoever's lock lands first gets it; the other is rejected cleanly, not left in an inconsistent state.
Requirements-and-class-diagram walkthrough covering members, books, and transactions
Clear requirements decomposition before jumping to classes
Public treatment doesn't consistently distinguish title-level Book from copy-level BookItem, and fine logic is presented as a fixed formula rather than a swappable strategy
The requirements-first framing is useful; this document's Book/BookItem split and Strategy-based fines go further
A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.
// Library Management System - full LLD implementation (single-file demo).
// No "Concurrency" badge -> single-threaded; the "two members racing the last copy"
// requirement is satisfied by checkout() being one atomic status-check-and-flip step
// against a single BookItem, not by explicit locking primitives.
//
// Design notes:
// - Book (title metadata) vs. BookItem (physical copy) is the central modeling
// decision: metadata is shared/immutable across copies, status is per-copy and
// constantly changing.
// - Observer for holds: BookItem.returnItem() notifies the title's HoldQueue, which
// pops the next Hold in FIFO order and hands the copy to that member - decoupling
// "a copy became available" from "who reacts to that".
// - Strategy for fines: FineStrategy.calculateFine(loan) isolates rate changes from
// Loan/Library's record-keeping code.
import java.util.*;
public class p15 {
enum ItemStatus { AVAILABLE, CHECKED_OUT, ON_HOLD, LOST }
enum Category { FICTION, NON_FICTION, REFERENCE }
enum MembershipType { REGULAR, PREMIUM }
// ---------- Book (title-level metadata) ----------
static class Book {
final String isbn, title, author;
final Category category;
Book(String isbn, String title, String author, Category category) {
this.isbn = isbn; this.title = title; this.author = author; this.category = category;
}
}
// ---------- BookItem (one physical, lendable copy) ----------
static class BookItem {
final String barcode;
final Book book;
ItemStatus status = ItemStatus.AVAILABLE;
Date dueDate;
final Library library; // back-reference used to reach the right HoldQueue on return
BookItem(String barcode, Book book, Library library) {
this.barcode = barcode; this.book = book; this.library = library;
}
// Marks the copy checked out; Library owns creating/recording the Loan itself.
void checkout(Date dueDate) {
this.status = ItemStatus.CHECKED_OUT;
this.dueDate = dueDate;
}
// Flips back to available, then defers to the title's HoldQueue (Observer) to
// decide whether this copy should instead go straight to the next waiting member.
void returnItem() {
this.status = ItemStatus.AVAILABLE;
this.dueDate = null;
HoldQueue queue = library.holdQueueFor(book);
if (queue != null && queue.hasWaiting()) {
Hold nextHold = queue.popNext();
this.status = ItemStatus.ON_HOLD; // reserved, not generally available
queue.notifyNext(this, nextHold);
}
}
}
// ---------- Member ----------
static class Member {
final String id;
final MembershipType membershipType;
final List<Loan> activeLoans = new ArrayList<>();
final List<Hold> activeHolds = new ArrayList<>();
Member(String id, MembershipType membershipType) { this.id = id; this.membershipType = membershipType; }
}
// ---------- Loan (one active checkout record) ----------
static class Loan {
final Member member;
final BookItem bookItem;
final Date checkoutDate;
final Date dueDate;
boolean returned = false;
Loan(Member member, BookItem bookItem, Date checkoutDate, Date dueDate) {
this.member = member; this.bookItem = bookItem; this.checkoutDate = checkoutDate; this.dueDate = dueDate;
}
boolean isOverdue(Date asOf) { return !returned && asOf.after(dueDate); }
}
// ---------- Hold (one member's place in a title's wait queue) ----------
static class Hold {
final Member member;
final Book book;
final long timestamp;
Hold(Member member, Book book, long timestamp) { this.member = member; this.book = book; this.timestamp = timestamp; }
}
// ---------- HoldQueue (FIFO wait list + Observer subject for availability) ----------
static class HoldQueue {
private final Deque<Hold> queue = new ArrayDeque<>();
void addHold(Hold hold) { queue.addLast(hold); }
boolean hasWaiting() { return !queue.isEmpty(); }
Hold popNext() { return queue.pollFirst(); }
int waitingCount() { return queue.size(); }
// The "reaction" side of the Observer relationship: BookItem triggers this,
// HoldQueue decides who gets notified. A real system would email/page the
// member here; we just print, and record it as satisfied.
void notifyNext(BookItem item, Hold hold) {
hold.member.activeHolds.remove(hold);
System.out.println(" Notifying " + hold.member.id + ": your hold on \"" + item.book.title + "\" is ready (barcode " + item.barcode + ")");
}
}
// ---------- FineStrategy (interface, Strategy) ----------
interface FineStrategy {
double calculateFine(Loan loan, Date asOf);
}
// Rate depends on both member type and item category - isolated here so a rate
// change never touches Loan or Library.
static class StandardFineStrategy implements FineStrategy {
public double calculateFine(Loan loan, Date asOf) {
if (!loan.isOverdue(asOf)) return 0.0;
long overdueDays = (asOf.getTime() - loan.dueDate.getTime()) / (1000L * 60 * 60 * 24) + 1;
double perDay = loan.bookItem.book.category == Category.REFERENCE ? 1.0 : 0.25;
if (loan.member.membershipType == MembershipType.PREMIUM) perDay *= 0.5; // premium discount
return overdueDays * perDay;
}
}
// ---------- Catalog (search index over Book entries) ----------
static class Catalog {
private final List<Book> books = new ArrayList<>();
void addBook(Book book) { books.add(book); }
List<Book> search(String query) {
String q = query.toLowerCase();
List<Book> results = new ArrayList<>();
for (Book b : books) {
if (b.title.toLowerCase().contains(q) || b.author.toLowerCase().contains(q)
|| b.category.name().toLowerCase().contains(q)) {
results.add(b);
}
}
return results;
}
}
// ---------- Library (facade) ----------
static class Library {
final Catalog catalog = new Catalog();
final Map<String, BookItem> itemsByBarcode = new HashMap<>();
final Map<String, Loan> activeLoanByBarcode = new HashMap<>();
final Map<String, HoldQueue> holdQueuesByIsbn = new HashMap<>();
final FineStrategy fineStrategy;
private static final long LOAN_PERIOD_MILLIS = 14L * 24 * 60 * 60 * 1000; // 14 days, flat for demo
Library(FineStrategy fineStrategy) { this.fineStrategy = fineStrategy; }
void addBookItem(BookItem item) { itemsByBarcode.put(item.barcode, item); }
HoldQueue holdQueueFor(Book book) { return holdQueuesByIsbn.get(book.isbn); }
List<Book> search(String query) { return catalog.search(query); }
// Single atomic status-check-and-flip against one BookItem - this is exactly
// what stops two members both succeeding against what they each believe is
// "the last copy".
Loan checkout(Member member, String barcode) {
BookItem item = itemsByBarcode.get(barcode);
if (item == null) throw new IllegalArgumentException("No such item: " + barcode);
if (item.status != ItemStatus.AVAILABLE) return null; // reject: not free to check out
Date due = new Date(System.currentTimeMillis() + LOAN_PERIOD_MILLIS);
item.checkout(due);
Loan loan = new Loan(member, item, new Date(), due);
member.activeLoans.add(loan);
activeLoanByBarcode.put(barcode, loan);
return loan;
}
void returnItem(String barcode) {
BookItem item = itemsByBarcode.get(barcode);
if (item == null) throw new IllegalArgumentException("No such item: " + barcode);
Loan loan = activeLoanByBarcode.remove(barcode);
if (loan != null) {
loan.returned = true;
loan.member.activeLoans.remove(loan);
}
item.returnItem(); // triggers the HoldQueue Observer hook internally
}
double calculateFine(Loan loan) { return fineStrategy.calculateFine(loan, new Date()); }
// Places a hold only when no copy is free; otherwise the member should just checkout.
void placeHold(Member member, Book book) {
HoldQueue queue = holdQueuesByIsbn.computeIfAbsent(book.isbn, k -> new HoldQueue());
Hold hold = new Hold(member, book, System.currentTimeMillis());
queue.addHold(hold);
member.activeHolds.add(hold);
}
}
// ---------- Demo ----------
public static void main(String[] args) throws InterruptedException {
Library library = new Library(new StandardFineStrategy());
Book book = new Book("978-1", "The Pragmatic Programmer", "Hunt & Thomas", Category.NON_FICTION);
library.catalog.addBook(book);
BookItem copy1 = new BookItem("BC-1", book, library);
BookItem copy2 = new BookItem("BC-2", book, library);
library.addBookItem(copy1);
library.addBookItem(copy2);
System.out.println("Search 'pragmatic': " + library.search("pragmatic").size() + " result(s)");
Member alice = new Member("alice", MembershipType.REGULAR);
Member bob = new Member("bob", MembershipType.PREMIUM);
Member carol = new Member("carol", MembershipType.REGULAR);
Loan loan1 = library.checkout(alice, "BC-1");
Loan loan2 = library.checkout(bob, "BC-2");
System.out.println("Alice checked out BC-1: " + (loan1 != null));
System.out.println("Bob checked out BC-2: " + (loan2 != null));
// Both copies are gone now - Carol must queue for a hold instead of a 3rd checkout.
Loan failedLoan = library.checkout(carol, "BC-1");
System.out.println("Carol tries BC-1 while checked out: " + (failedLoan == null ? "rejected as expected" : "unexpectedly succeeded"));
library.placeHold(carol, book);
System.out.println("Carol placed a hold; queue size: " + library.holdQueueFor(book).waitingCount());
// Alice returns her copy -> Observer hook should notify Carol and reserve BC-1 for her.
library.returnItem("BC-1");
System.out.println("BC-1 status after return: " + copy1.status);
// Simulate an overdue fine: backdate the due date on Bob's loan and price it.
loan2.dueDate.setTime(System.currentTimeMillis() - (3L * 24 * 60 * 60 * 1000));
double fine = library.calculateFine(loan2);
System.out.println("Bob's fine for 3 overdue days (premium discount applied): $" + fine);
}
}