Library management system
Design a library system with members, books and their physical copies, loans with due dates, fines for late returns, and reservations for books that are all out.
Design a library management system. Members borrow and return books, pay fines when late, and reserve a book when every copy is out. Librarians add books and copies. Show the classes and the checkout and return code.
Questions to ask first
- Can the library have many copies of one book? Assume yes. A member borrows a copy, but reserves a book (any copy will do).
- How many books can a member borrow, and for how long? Assume 5 books for 14 days, with different limits per membership type later.
- How are fines computed? Assume ₹10 per late day, capped at ₹500 per loan. Stored in paise.
- Can a member borrow with unpaid fines? Assume no, if unpaid fines are over ₹100.
- How do reservations work? Assume first come, first served. A returned copy is kept for the next member for 3 days.
- Do we need search? Assume simple search by title, author and ISBN through a Catalog class.
Requirements
- Add books (ISBN, title, authors) and their physical copies, each with a barcode.
- Check out a copy to a member if they are under their limit and have no large unpaid fines.
- Return a copy: close the loan, compute any fine, and pass the copy to the next reservation.
- Reserve a book when no copy is available; serve reservations in order.
- Mark loans overdue after the due date, and let members pay fines.
- Search the catalog by title, author or ISBN.
Out of scope
- E-books and digital lending.
- Buying books and supplier orders.
- Sending emails or SMS (we call a notifier interface).
Entities
- Book: isbn, title, authors (the title, not the paper)
- BookCopy: barcode, book, CopyStatus (AVAILABLE, LOANED, ON_HOLD, LOST)
- Member: id, name, maxLoans, activeLoanCount, unpaidFinePaise -> canBorrow()
- Loan: copy, member, issuedOn, dueOn, returnedOn, LoanStatus -> close()
- Reservation: book, member, createdAt
- Fine: loan, amountPaise, paid; FinePolicy -> finePaise(loan, returnedOn)
- Catalog -> search(); LibraryService -> checkout(), returnCopy(), reserve()
Classes in the model design
- LibraryService (class)
- Catalog (class)
- Book (class)
- BookCopy (class)
- CopyStatus (enum)
- Member (class)
- Loan (class)
- LoanStatus (enum)
- Reservation (class)
- Fine (class)
- FinePolicy (interface)
- PerDayFinePolicy (class)
The main use case, step by step
- Kiran brings copy B-1042 of 'Clean Code' to the desk. The librarian scans it and the app calls `LibraryService.returnCopy(copy)`.
- The service finds the active `Loan` for barcode B-1042, calls `loan.close(today)` and reduces Kiran's active loan count by one. The loan status becomes RETURNED.
- The service asks `FinePolicy.finePaise(loan, today)`. The copy is 4 days late, so `PerDayFinePolicy` returns 4000 paise. The service creates a `Fine` and adds it to Kiran's unpaid total.
- The service checks the reservation queue for this book's ISBN. Neha reserved it last week, so she is first.
- The copy does not go back to AVAILABLE. `copy.holdFor(neha, today + 3 days)` sets it to ON_HOLD for Neha only, and the notifier tells her it is ready.
- When Neha arrives, `checkout(neha, copy)` checks `neha.canBorrow()` and that the copy is held for her, then creates a new 14-day `Loan`.
Patterns used
- Strategy: FinePolicy with PerDayFinePolicy, passed into LibraryService.. Fine rules change: a cap, a grace day, lower fines for students. Each rule is a class with its own test. The return flow does not change when the rule does.
- State (as enums with guarded moves): Loan moves through ACTIVE, OVERDUE, RETURNED and LOST; BookCopy through AVAILABLE, LOANED, ON_HOLD and LOST.. Each object changes status only through its own methods, which reject invalid moves such as returning a loan twice. A state diagram makes the allowed moves easy to review.
Key code
Fine policy
public interface FinePolicy {
long finePaise(Loan loan, LocalDate returnedOn);
}
public final class PerDayFinePolicy implements FinePolicy {
private final long paisePerDay;
private final long capPaise;
public PerDayFinePolicy(long paisePerDay, long capPaise) {
this.paisePerDay = paisePerDay;
this.capPaise = capPaise;
}
@Override
public long finePaise(Loan loan, LocalDate returnedOn) {
long lateDays = ChronoUnit.DAYS.between(loan.dueOn(), returnedOn);
if (lateDays <= 0) return 0;
return Math.min(Math.multiplyExact(lateDays, paisePerDay), capPaise);
}
}
Checkout and return
public final class LibraryService {
private static final int LOAN_DAYS = 14;
private static final int HOLD_DAYS = 3;
private final Map<String, Loan> activeLoans = new HashMap<>(); // by barcode
private final Map<String, Queue<Reservation>> waiting = new HashMap<>(); // by ISBN
private final FinePolicy finePolicy;
private final Clock clock;
public LibraryService(FinePolicy finePolicy, Clock clock) {
this.finePolicy = finePolicy;
this.clock = clock;
}
public Loan checkout(Member m, BookCopy copy) {
if (!m.canBorrow()) throw new IllegalStateException("Loan limit reached or fines unpaid");
LocalDate today = LocalDate.now(clock);
copy.lendTo(m); // throws unless AVAILABLE, or ON_HOLD for this member
Loan loan = new Loan(copy, m, today, today.plusDays(LOAN_DAYS));
activeLoans.put(copy.barcode(), loan);
m.startLoan();
return loan;
}
public Optional<Fine> returnCopy(BookCopy copy) {
Loan loan = activeLoans.remove(copy.barcode());
if (loan == null) throw new IllegalStateException("Copy " + copy.barcode() + " is not on loan");
LocalDate today = LocalDate.now(clock);
loan.close(today);
loan.member().endLoan();
Reservation next = waiting.getOrDefault(copy.book().isbn(), new ArrayDeque<>()).poll();
if (next != null) copy.holdFor(next.member(), today.plusDays(HOLD_DAYS)); // then notify
else copy.markAvailable();
long paise = finePolicy.finePaise(loan, today);
if (paise == 0) return Optional.empty();
Fine fine = new Fine(loan, paise);
loan.member().addFine(fine);
return Optional.of(fine);
}
}
Deep dives
Book versus copy
Weak: One Book class with a quantity field. Checkout does quantity--. You cannot tell which physical copy Kiran has, which one is damaged, or which one is lost. A loan has nothing concrete to point to.
Good: Split it into Book (ISBN, title, authors: the same for every copy) and BookCopy (barcode, status: one object per physical item). A Loan points to one BookCopy. Availability is the count of copies with status AVAILABLE.
Great: Do the same split, and point a Reservation at the Book, not at a copy. A member wants 'Clean Code', not copy B-1042. The service assigns a specific copy only when one comes back, by putting it ON_HOLD for that member. Lost and damaged copies change only their own status.
Trade-off: Two classes take a little more code, and every screen must decide whether it talks about titles or copies. That is the real-world model, so the cost is worth it. A pure e-book library might not need copies at all.
Fines and money
Weak: Compute the fine inside returnBook with double fine = days * 10.0, and add it to member.fine. The rule is hard-coded, money is a floating-point number, and there is no record of which loan caused which fine.
Good: Put the rule behind FinePolicy and return paise as a long. The return flow calls the policy and does not know the rate. A test can check '4 days late gives 4000 paise' without a full return flow.
Great: Do the same, and store each fine as a Fine object linked to its loan, with a paid flag. The member's unpaid total is the sum of unpaid fines. Choose the policy per membership type (students pay less), add a cap and a grace day, and inject a Clock so tests can control 'today'.
Trade-off: Separate Fine records make the data model bigger, but librarians need them to answer 'why do I owe ₹40?' and to waive one fine. A plain running total is simpler, but it cannot explain itself.
Fair reservations
Weak: On return, scan all reservations for any that match the book and pick one at random, or pick none and set the copy to AVAILABLE. The next person at the desk takes it, and the member who waited a week loses out.
Good: Keep a FIFO queue (first in, first out) of reservations per ISBN. On return, give the copy to the head of the queue by setting it ON_HOLD for that member. Checkout of an ON_HOLD copy is allowed only for that member.
Great: Do the same, and give the hold an end date. A daily job (or a check at read time) moves an expired hold to the next member in the queue, or back to AVAILABLE. Limit reservations per member, and cancel a reservation when the member borrows another copy of the same book.
Trade-off: Holding a returned copy for 3 days keeps it off the shelf even when the reserving member never comes. A shorter hold is fairer to walk-in members; a longer hold is kinder to the member who reserved. Make the hold length a setting.
Now add one more feature
- Now add student and faculty members with different limits and fines. What changes? Add a `MembershipPlan` (max loans, loan days, `FinePolicy`) that each Member holds. Checkout reads limits from the plan. No `if` on member type appears in the service.
- Now let members renew a loan once, if nobody has reserved the book. What changes? Add `renew(loan)` to the service. It checks the reservation queue for that ISBN and the renew count on Loan, then moves `dueOn` forward.
- Now send a reminder two days before the due date and when a reserved copy is ready. What changes? The service publishes events (DueSoon, CopyReady) to a `Notifier` interface. An observer sends SMS or email, so the library logic does not depend on any channel.
- Now support several branches with transfers between them. What changes? `BookCopy` gets a home branch and a current branch, and a `Transfer` class records moves. Search shows availability per branch.
What each level is expected to show
SDE-1: Separate Book and BookCopy, model Member, Loan and Reservation, and write working checkout and return with limits and a per-day fine in paise. Use enums for copy and loan status. A main method that borrows, returns late and shows the fine is enough.
SDE-2: Everything at SDE-1, plus a fine policy chosen per membership type, a fair reservation queue with hold expiry, an injected Clock, and clear status transitions that reject invalid moves. Explain how notifications and renewals plug in without editing the core flow.
Common mistakes
- Using one Book class with a quantity, so loans cannot point to a physical copy.
- Making a reservation point to a specific copy instead of the book title.
- Hard-coding the fine rule and using `double` for money.
- Calling `LocalDate.now()` everywhere, so tests cannot check late returns.
- Setting a returned copy to AVAILABLE when someone is waiting for it.
- Not checking the member's loan limit and unpaid fines before checkout.