Design a Library Management System
A library management system works by separating the abstract thing a reader searches for (a Book — one ISBN, one title) from the physical thing they carry home (a BookItem — one barcode, one copy on one rack), so that loans, reservations, fines, and availability are all tracked per copy while search and catalog stay per title. Almost every requirement in this problem — "check out any copy", "reserve when none available", "who has this exact book" — falls out cleanly once you make that one split. Get the split wrong and the whole model fights you.
The core decomposition (and why)
The single most important modeling decision is the Book vs BookItem distinction. A reader searches the catalog for "Designing Data-Intensive Applications" — that is one Book (ISBN 978-1449373320). But the library owns three physical copies, each with its own barcode, rack, and loan state. Those are three BookItems. This is why each layer holds different responsibilities:
| Concept | Identity | Owns | Why it lives here |
|---|---|---|---|
Book | ISBN | title, authors, subject, publisher | Search and dedup happen per title, not per copy |
BookItem | barcode | status, dueDate, rack, format, price | You loan, reserve, and fine a specific copy |
BookLending | (barcode, member) | creationDate, dueDate, returnDate | An event record — answers "who, when, due when" |
BookReservation | (barcode, member) | status, queue position | Decouples "I want it" from "it's available" |
Fine | (member, lending) | amount, paid | Money is auditable; never a field on the loan |
An alternative some candidates reach for is folding lending and reservation state into boolean flags on BookItem (isLoaned, reservedBy). It compiles and demos fine — but it loses history (you can't answer "who borrowed this last March"), it can't model a reservation queue, and it can't represent "returned late, fine pending" without a third flag. Modeling lending and reservation as their own records keeps BookItem a thin state holder and pushes the workflow into objects you can query and audit.
Worked example: returning a book 3 days late
The fine path is where the original code was broken, so trace it with real values. Member M-42 borrowed copy LIB-0002; the library allows MAX_LENDING_DAYS = 10. Assume a flat fine of $0.50 per day late. The dates:
| Step | Value |
|---|---|
| Borrowed on | 2026-06-10 |
| Due date (borrowed + 10) | 2026-06-20 |
| Actually returned (today) | 2026-06-23 |
today.compareTo(dueDate) | > 0 → overdue, enter fine branch |
diff = today.getTime() - dueDate.getTime() | 259,200,000 ms |
diffDays = diff / (24*60*60*1000) | 3 days |
Fine.collectFine(memberId, 3) | 3 × $0.50 = $1.50 |
That arithmetic only runs if the code names the right variable. The original wrote today.compareTo(...) on one line and then todayDate.getTime() on the next — two different names for the same instant. It does not compile, so the fine is never computed.
Why the naive version is wrong
The shipped code had three defects that all prevent compilation, plus a behavioral bug. Each is small, and each is the kind of thing an interviewer will catch:
- Undeclared variable.
checkForFinedeclaresDate today = new Date();then usestodayDate.getTime().todayDatewas never declared — compile error. (It also readsmemberId, which isn't in scope; the fix threads it through from the lending record.) - Phantom enum constant. Both
checkoutBookItemandrenewBookItemcallbookReservation.updateStatus(ReservationStatus.COMPLETED), butReservationStatusonly definesWAITING, PENDING, CANCELED, NONE.COMPLETEDdoesn't exist — compile error, and a real modeling gap: there was no way to mark a reservation fulfilled. The fix addsCOMPLETED. - Non-existent type.
public bool renewBookItem(...)— Java has nobooltype; the keyword isboolean. Compile error.
Here is the corrected Member class. The enum gains COMPLETED, today is used consistently, memberId is read from the lending record, and renewBookItem returns boolean:
public enum ReservationStatus {
WAITING, PENDING, CANCELED, COMPLETED, NONE // added COMPLETED
}
public class Member extends Account {
private Date dateOfMembership;
private int totalBooksCheckedout;
public int getTotalBooksCheckedout() { return totalBooksCheckedout; }
private void incrementTotalBooksCheckedout() { totalBooksCheckedout++; }
private void decrementTotalBooksCheckedout() { totalBooksCheckedout--; }
public boolean checkoutBookItem(BookItem bookItem) {
if (getTotalBooksCheckedout() >= Constants.MAX_BOOKS_ISSUED_TO_A_USER) {
ShowError("User already has the maximum number of books checked out");
return false;
}
BookReservation reservation =
BookReservation.fetchReservationDetails(bookItem.getBarcode());
// reserved by someone else -> refuse
if (reservation != null && !reservation.getMemberId().equals(getId())) {
ShowError("This book is reserved by another member");
return false;
} else if (reservation != null) {
// this member's own reservation is now fulfilled
reservation.updateStatus(ReservationStatus.COMPLETED);
}
if (!bookItem.checkout(getId())) return false;
incrementTotalBooksCheckedout();
return true;
}
private void checkForFine(String bookItemBarcode) {
BookLending lending = BookLending.fetchLendingDetails(bookItemBarcode);
Date dueDate = lending.getDueDate();
Date today = new Date();
if (today.compareTo(dueDate) > 0) { // overdue
long diff = today.getTime() - dueDate.getTime(); // was todayDate
long diffDays = diff / (24L * 60 * 60 * 1000);
Fine.collectFine(lending.getMemberId(), diffDays); // memberId from record
}
}
public void returnBookItem(BookItem bookItem) {
checkForFine(bookItem.getBarcode());
BookReservation reservation =
BookReservation.fetchReservationDetails(bookItem.getBarcode());
if (reservation != null) {
// someone is waiting -> hold it for them, notify
bookItem.updateBookItemStatus(BookStatus.RESERVED);
reservation.sendBookAvailableNotification();
} else {
bookItem.updateBookItemStatus(BookStatus.AVAILABLE);
}
decrementTotalBooksCheckedout();
}
public boolean renewBookItem(BookItem bookItem) { // was 'bool'
checkForFine(bookItem.getBarcode());
BookReservation reservation =
BookReservation.fetchReservationDetails(bookItem.getBarcode());
// reserved by someone else -> cannot renew, hand it over
if (reservation != null && !reservation.getMemberId().equals(getId())) {
ShowError("This book is reserved by another member");
bookItem.updateBookItemStatus(BookStatus.RESERVED);
reservation.sendBookAvailableNotification();
return false;
} else if (reservation != null) {
reservation.updateStatus(ReservationStatus.COMPLETED);
}
BookLending.lendBook(bookItem.getBarcode(), getId());
bookItem.updateDueDate(LocalDate.now().plusDays(Constants.MAX_LENDING_DAYS));
return true;
}
}Note one behavioral fix beyond the compile errors: the original returnBookItem always ended by setting the item to AVAILABLE even after setting it to RESERVED for the next member, silently dropping the hold. The corrected version uses an else so a reserved copy stays reserved. It also decrements the member's count on return, which the original omitted — without it, a member who returns five books still shows five checked out and can never borrow again.
Pitfalls
- Comparing IDs with
==. The original wrotebookReservation.getMemberId() != this.getId(). IfmemberIdis aString,==compares object references, not values, so a member can be wrongly told their own reserved book belongs to someone else. Use.equals()(or make IDs a value type). - The set-then-overwrite status bug. Setting an item to
RESERVEDand then unconditionally toAVAILABLEin the same method makes the reservation queue silently leak — the next member is notified the book is ready, but the catalog shows it as free for anyone to grab. Guard mutually exclusive state transitions withelse. - Day count off-by-one and rounding. Integer division
diff / 86_400_000truncates: a book 47 hours late counts as 1 day, not 2. Decide deliberately whether you charge per started day (Math.ceil) or per completed day, and beware daylight-saving shifts — preferLocalDate.until(...)over millisecond subtraction for calendar-day math. - Forgetting to decrement on return/renew. The
MAX_BOOKS_ISSUEDlimit is enforced on checkout but only correct if returns decrement the counter. Miss it and active members lock themselves out. - Reservation has no queue. A single
reservedByfield can't model two members waiting on the last copy. Real systems keep an orderedBookReservationlist per book and notify the head of the queue on return.
Takeaways
- Split the abstract
Book(ISBN, searchable) from the physicalBookItem(barcode, loanable). Almost every requirement resolves once this split is right. - Model lending, reservation, and fine as their own records, not booleans on the item — you gain history, an auditable money trail, and a real reservation queue.
- State transitions that are mutually exclusive (RESERVED vs AVAILABLE) must be guarded with
else; counters touched on checkout must be undone on return. - In an interview, compiling Java still matters:
booleannotbool, declare what you use, and only reference enum constants that exist.
Re-authored and deepened for this guide. The Book/BookItem decomposition, use cases, and class roles follow the classic treatment in Grokking the Object Oriented Design Interview (Design Gurus / educative.io). The three compile defects (todayDate vs today, the missing ReservationStatus.COMPLETED, and bool vs boolean), the set-then-overwrite status bug, and the missing decrement-on-return were identified and fixed here; the fine worked example uses concrete dates. Java type and date-handling guidance per the Java Language Specification and java.time (JSR-310) documentation.
🤖 Don't fully get this? Learn it with Claude
Stuck on Design a Library Management System? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Design a Library Management System** (OO & Low-Level Design) and want to truly understand it. Explain Design a Library Management System from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Design a Library Management System** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Design a Library Management System** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Design a Library Management System** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.