If you remember five things
The whole answer in one glance. Everything below is the defense of these five calls.
- Book vs BookItem. Book is the catalog entry (title, author, ISBN). BookItem is one physical copy with a barcode. People borrow copies, not titles. This single split makes reservations, availability, and the last-copy race fall out naturally.
- State machine on the copy. AVAILABLE, LOANED, RESERVED, LOST are driven through guarded transition methods, never public setters, so nobody flips a copy to available while it is in someone's bag.
- Lock the copy, not the library. The two-people-one-last-copy race is resolved in exactly one synchronized method on the BookItem. Pre-checks run lock-free.
- Patterns the problem asked for. Strategy for fines, State for item status, Observer for reservation notifications, composition for Book to BookItem. Each one earns its place, none is decoration.
- Name your scope cuts. Payments, auth, and distributed inventory are parked out loud. Naming the cut is worth as much as the design.
I will walk through it the way I actually talk at a whiteboard: question first, model second, then the few lines of code that carry the whole design.
1Clarify the requirements#
Ask before you draw a box. Interviewers plant ambiguity on purpose, and an LMS is full of it. The order I ask in signals my priorities.
Candidate: Are we modeling a single physical branch, or a system across multiple libraries?
Interviewer: One branch for now. Keep the door open for more.
Candidate: Is there one copy per title, or many? This is the one that changes the whole model.
Interviewer: Many. The library owns several copies of popular titles.
Candidate: Last one: what can a member actually do, and what can I leave out?
Interviewer: Checkout, return, renew, reserve, and fines. Payments and auth are out of scope.
What those answers just told me:
- The real problem is the Book vs BookItem split and the borrow and return state machine, not CRUD.
- "Many copies" means I cannot model "Book" as the borrowable thing. It falls apart the moment two people want the same title.
- "One branch, keep it open" means I park multi-branch as a choice, not a case I missed.
Functional scope I lock in:
- Search the catalog by title, author, or subject.
- Check out a copy, return it, and renew a loan.
- Reserve a title that is currently all checked out.
- Get fined for late returns.
- A librarian can add or remove books and copies, and register members.
Limits that shape the design (I pin numbers so the code has something concrete to enforce):
- A member holds at most N books at once (say 5).
- A loan runs for a fixed period (say 14 days).
- Overdue items accrue a fine per day (say $0.25 a day).
Non-functional, mentioned briefly:
- The checkout path must be safe under concurrency (two librarians, one copy, same second).
- Catalog search should stay fast as the collection grows past hundreds of thousands of titles.
- The design should let us swap fine rules or add new item types without tearing things up.
Parked out of scope unless asked: payments integration, authentication, and distributed inventory.
I am scoring this opening before you write a line of Java. I want to hear you ask "one copy or many?" early and on your own. Name it late, or model "Book" as the borrowable thing, and I have quietly marked you as someone who has not built real inventory. Naming the scope cuts (payments, auth, distribution) counts for as much as the design itself.
2Identify the core entities#
Rule of thumb for what becomes a class. Every noun with its own lifecycle or its own identity earns a class. If a thing changes state over time, or needs to be looked up by an ID, it is a class. Otherwise it stays a field.
- Book is the abstract title. One row per ISBN. It does not get borrowed. Carries title, author, subject for search.
- BookItem is one physical copy with a barcode. It has a status: available, loaned, reserved, lost. This is the object that moves through the system.
- Member is a patron. Holds a list of active loans and a borrowing limit of 5.
- Librarian is staff. Mutates the catalog and manages members. Member and Librarian share identity fields (id, name, status), so they extend a common
Account. - Loan (I call it
BookLending) ties aBookItemto aMemberwith a due date 14 days out. The record of who has what and until when. - Reservation is a claim on a title when no copy is free. First in line gets notified when one comes back.
- Fine is money owed for an overdue return, computed at $0.25 per late day in my default rule.
- Catalog is the searchable index over all books.
- Library is the top-level coordinator that owns the catalog and wires the pieces together.
The Book vs BookItem split is the single most important decision here. Get it right and reservations, availability, and the last-copy race all fall out. Get it wrong and every later question turns into a patch.
3Class design#
A few relationships to state plainly before the diagram, because the diamonds and arrows carry meaning.
- Book composes BookItem. Kill the book, the copies have no meaning. That is composition.
- Member aggregates BookLending. The loans exist as historical records even after the member relationship ends. That is aggregation.
- Catalog aggregates Book. References, not ownership.
- Member and Librarian inherit Account. The "is-a" genuinely holds for shared identity and status.
Two pattern choices baked into the diagram:
- Strategy for the fine calculation, because "how much do we charge" is exactly the rule that changes per library or per member type.
- State (enum plus controlled transitions) for item status, so nobody can set a copy to "available" while it is physically in someone's bag.
4Key implementation#
I show the parts that carry the design and skip boilerplate getters. Start with the enums and the physical copy, because the state machine lives there and that is where the interview is won.
enum BookStatus { AVAILABLE, LOANED, RESERVED, LOST }
enum AccountStatus { ACTIVE, BLOCKED, CLOSED }
class BookItem {
private final String barcode;
private final Book book;
private BookStatus status = BookStatus.AVAILABLE;
private Date dueDate;
synchronized boolean reserveFor(Member m) {
if (status != BookStatus.AVAILABLE) return false;
status = BookStatus.RESERVED;
return true;
}
synchronized boolean markLoaned(Date due) {
if (status != BookStatus.AVAILABLE && status != BookStatus.RESERVED)
return false;
status = BookStatus.LOANED;
this.dueDate = due;
return true;
}
synchronized void markReturned() {
status = BookStatus.AVAILABLE;
dueDate = null;
}
}
Why this shape:
- Status transitions are methods on the item, not public setters. The state machine is enforced at the only place it can be violated: the object that owns the state.
- You cannot loan an item that is already loaned. The guard rejects it.
- The method returns
falseinstead of throwing, so the caller decides what to do.
The fine rule is a Strategy, so you swap it without touching the return path:
interface FineStrategy {
double compute(long daysLate);
}
class FlatDailyFine implements FineStrategy {
private final double perDay;
FlatDailyFine(double perDay) { this.perDay = perDay; }
public double compute(long daysLate) {
return daysLate <= 0 ? 0.0 : daysLate * perDay;
}
}
Reveal the answer
Now the Library, the coordinator. This is where the interesting concurrency lives, so I slow down and narrate every line.
class Library {
private final Catalog catalog;
private final FineStrategy fineStrategy;
private final int loanDays;
private final Map<String, BookItem> itemsByBarcode;
BookLending checkout(Member member, String barcode) {
if (member.getStatus() != AccountStatus.ACTIVE)
throw new IllegalStateException("member not active");
if (member.activeLoanCount() >= member.getMaxBooks())
throw new IllegalStateException("borrow limit reached");
BookItem item = itemsByBarcode.get(barcode);
if (item == null) throw new NoSuchElementException("unknown copy");
Date due = plusDays(new Date(), loanDays);
// the single atomic gate: only one thread wins this
if (!item.markLoaned(due))
throw new IllegalStateException("copy not available");
BookLending lending = new BookLending(item, member, new Date(), due);
member.addLoan(lending);
return lending;
}
Fine returnItem(String barcode) {
BookItem item = itemsByBarcode.get(barcode);
BookLending lending = item.getActiveLending();
lending.setReturnedOn(new Date());
item.markReturned();
lending.getMember().removeLoan(lending);
long daysLate = daysBetween(lending.getDueDate(), new Date());
double owed = fineStrategy.compute(daysLate);
Fine fine = owed > 0 ? new Fine(owed, lending.getMember()) : null;
notifyNextReservation(item); // Observer kicks in here
return fine;
}
}
The line I want the interviewer to notice:
- The last-copy race is resolved in exactly one place,
item.markLoaned(...), synchronized on the item. - Everything before it (member-active check, limit check) is a cheap pre-check that runs without a lock.
- I do not lock the whole library. That would serialize every checkout in the building on one monitor and kill throughput.
- Lock the smallest thing that can go wrong: the one copy two people want.
Most candidates either forget the race entirely or reach for a lock on the Library and call it done. Naming the race, placing the lock on the single BookItem, and explaining why the pre-checks stay lock-free shows you understand that contention is a per-copy problem, not a per-system one. That is a judgment signal, not a syntax one.
5Design decisions and patterns used#
Strategy for fines.
- Different libraries charge differently, and some waive fines for certain member tiers.
FineStrategyas an interface means a new rule is a new class, not a rewrite ofreturnItem.FlatDailyFineat $0.25 a day is one implementation. A tiered or capped rule is just another.
State for BookItem.
- Enum plus guarded transition methods, not a full state-object hierarchy.
- For four states (available, loaned, reserved, lost) that is the right weight.
- If the states grew behavior-heavy, I would promote each to its own class implementing a
BookStateinterface. I say that trade-off out loud so the interviewer knows I chose against the heavier version on purpose.
Observer for reservations.
- When a copy comes back, the member who reserved the title first should be notified.
- The reservation queue subscribes to the item's "returned" event.
- This decouples "a book came back" from "send someone an email," so adding SMS or push later does not touch
returnItem.
Factory (lightweight) for creating accounts and items.
- Construction rules (assign a barcode, set default status to AVAILABLE) live in one spot instead of being copy-pasted at every call site.
Composition over inheritance for Book and BookItem.
- I resisted
BookItem extends Book, a common wrong turn. A copy is not a kind of book, it is a physical instance of one. - Inheritance here forces every copy to duplicate title and author, and breaks the moment you ask "how many copies are on loan."
- The one inheritance I kept is
MemberandLibrarianfromAccount, because they genuinely share identity and status and the "is-a" holds.
6Handling edge cases and concurrency#
This is where a medium problem earns its difficulty. I try to raise these before the interviewer does.
- Two members, one last copy. The synchronized transition on the single
BookItemis the gate. Pre-checks (member active, under limit) are optimistic and cheap; the real commit is atomic. On a real distributed backend I would back this with an optimistic version column or a row lock, and I would say so rather than pretendsynchronizedscales across servers. - Reserve then nobody shows up. A reservation should expire. I give it a
createdOnand a TTL; a background sweep cancels stale reservations and frees the item. Otherwise one member freezes a copy forever by reserving and never collecting. - Return of an item that was reserved. On return I do not just flip to AVAILABLE. I check the reservation queue for that title. If someone is waiting, the item goes to RESERVED and the Observer fires a notification, with its own hold window so the next person has a fair chance to collect.
- Overdue at checkout time. If a member has unpaid fines or an overdue item, I block new checkouts. That check belongs in
checkout, before the atomic gate, so we fail fast without ever touching the item's lock. - Renewals. A renew is only allowed if nobody has reserved the title. If there is a queue, renewal is denied, which keeps the line fair. The holder does not get to jump the reservation queue indefinitely.
- Lost items. A copy can go to LOST, which charges the replacement cost and removes it from available inventory without deleting the historical loan record. The loan still happened; we keep it for audit and member history.
- Clock and timezone. Fines depend on dates. I normalize to the library's timezone and compute day boundaries carefully, because "one day late" at 00:01 is a real argument with a real customer standing at the desk.
Claiming checkout is "thread-safe" because a method says synchronized, without saying what it is synchronized on or why the Library-wide lock would wreck throughput. Worse: flipping an item straight to AVAILABLE on return and forgetting the reservation queue, so the person who waited in line never gets the copy. Walk the last-copy race and the reservation-on-return flow calmly and you have shown the judgment they are hiring for.
7Extensions and follow-ups#
When the interviewer pushes, each extension reuses the model rather than rewriting it.
- Multiple branches.
Librarybecomes one of many under aLibrarySystem. ABookItemnow belongs to a branch. Inter-branch transfers become a new flow. The catalog aggregates across branches while availability stays per-branch, because a copy is physically in one building. - Membership tiers. Premium members get longer loans and higher limits. Tier drives
maxBooksandloanDays, and can select a differentFineStrategy. No new plumbing, just different values flowing into code that already exists. - E-books and audio. This is why keeping BookItem separate pays off. A digital item has unlimited copies and no physical return, so I introduce a
DigitalItemwith a license-expiry instead of a due date, both under a commonLendableIteminterface. The last-copy race simply does not apply, which the type makes obvious. - Search at scale. My in-memory
Catalogwith maps is fine for the interview. In production I would push search to an inverted index (think Elasticsearch) and keep the maps only for exact barcode and ISBN lookups, which stay O(1) and do not belong in a text engine. - Notifications. The Observer already generalizes: overdue reminders, reservation-ready alerts, and "new arrival" subscriptions all become observers on different events, so the notification surface grows without the core flows knowing about it.
"Two people grab the last copy at the same instant. Who wins and why?"
"Why not just make BookItem extend Book?"
"A copy comes back and three people reserved the title. What happens?"
"How do you add e-books without rewriting the borrow flow?"
"Your synchronized keyword only works in one process. What about real scale?"
8What the interviewer is really checking#
Honestly? Whether you catch the Book vs BookItem distinction without being told. That one decision separates people who have modeled real systems from people reciting CRUD. After that:
- Lock the right thing (the item, not the world) for concurrency.
- Pick patterns because the problem asked for them, not to show off.
- Name your scope cuts clearly.
- Talk through the last-copy race and the reservation-on-return flow calmly.
Do those and you have shown the judgment they are hiring for.
What you can now explain in the room
- Why Book and BookItem must be separate, and how that makes reservations, availability, and the last-copy race fall out.
- Why the item status is a guarded state machine instead of public setters.
- Where exactly the concurrency lock goes, and why locking the whole Library kills throughput.
- Why Strategy fits fines, Observer fits reservation notifications, and composition beats inheritance for copies.
- How reservations, renewals, lost items, and overdue checkouts each change the borrow and return flow.
- How the model extends to multiple branches, membership tiers, and e-books without a rewrite.
If you liked this one, the same muscles show up in Design a Parking Lot (state and inventory), the Observer pattern deep-dive, and Design a Movie Ticket Booking System (concurrent seat holds).
Get a prep plan built around your Design a Library Management System interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.