Design a Library Management System, LLD Interview
A library system. A Book is the title, and a BookCopy is one item on the shelf. Members borrow copies within limits set by their type. A returned copy goes to the first member waiting for it. Late fines come from a rule the library can change.
Where it shows up
A classic object design question in LLD rounds and college placement interviews in India. It is a common starter problem at service and product companies alike.
Why this is asked
Its value is in the modelling. A strong candidate first splits a book from a copy. A book is the title, with an ISBN. A copy is one physical item with a barcode. A library has many copies of one title. And loans are of copies, not titles. After that, the question tests two more things. Are rules that change, like fines and loan limits, kept out of the main flow? And is the hold queue fair, with no way to jump it?
Requirements
Functional
- Add books and their physical copies to the catalogue.
- Search the catalogue by title.
- A member borrows a free copy of a title and gets a due date.
- Members have a loan limit and a loan period. Both depend on their type, student or faculty.
- A member returns a copy. A late return is charged a fine.
- When no copy is free, a member can place a hold. They join a first-come, first-served queue.
- When a copy with holds comes back, it is kept for the first member in the queue. That member is told.
Constraints & non-functional
- A loan is of one physical copy, never of a title in general.
- A copy is in exactly one state at a time: free, on loan, on the hold shelf, or lost.
- A copy kept for a hold can only be collected by the member it is kept for.
- Members with too many loans, or too many unpaid fines, cannot borrow.
- The fine rule can change without changing the service.
- Returning the same loan twice must not double the fine or free the copy twice.
- This is in-memory LLD. Storage is hidden behind the service, and its methods are locked.
Core classes & entities
LibraryService
The single way in for borrowing, returns, holds and search. It enforces every rule in one place, under one lock.
attrs: books: Map<isbn, Book>, copiesByIsbn: Map<isbn, List<BookCopy>>, holdQueue: Map<isbn, Deque<Member>>, fines: FinePolicy
methods: addCopy(book, barcode), borrow(member, isbn, today): Loan, giveBack(loan, today): long, placeHold(member, isbn)
Book
A title: the ISBN, the title and the author. All its copies share it.
attrs: isbn, title, author
BookCopy
One physical item, with its own barcode and its own state. This is what is actually lent.
attrs: barcode, book: Book, status: CopyStatus, heldFor: Member
Member
Someone who borrows. Their type sets their loan limit and loan period. They have active loans and unpaid fines.
attrs: id, type: MemberType, activeLoans: List<Loan>, finesPaise
methods: maxLoans(), loanDays()
Loan
One copy lent to one member. It records when it was borrowed, when it is due, and when it came back.
attrs: copy: BookCopy, member: Member, borrowed, due, returned
FinePolicy
Decides the fine for a loan returned on a given day. PerDayFine charges a set amount per late day, up to a cap.
methods: fineFor(loan, returnedOn): long
Notifier
Tells a member something happened, like a held copy being ready. Here it is InboxNotifier. A real library could use email or SMS.
methods: notify(member, message)
Relationships
- BookCopy → association → Book. Many copies of one title.
- Loan → association → BookCopy. A loan is of one physical copy.
- Loan → association → Member. A member can have several loans, up to their limit.
- LibraryService → composition → BookCopy. The service owns the list of copies.
- LibraryService → association → FinePolicy. Passed in, so the fine rule can change.
- LibraryService → association → Notifier. Passed in, so the way of telling members can change.
- FinePolicy → implements → PerDayFine. One class per fine rule.
Design patterns used
Strategy in FinePolicy with PerDayFine
Fine rules differ between libraries and change over time. A new rule is a new class.
Observer in Notifier, called when a held copy comes back
The service says a copy is ready. It does not need to know if the message goes by app, email or SMS.
Facade in LibraryService
Every rule is enforced in one place. So no caller can lend a held copy or skip the loan limit.
Type apart from the item in Book and BookCopy
The title's facts are stored once. Each physical copy carries only its own state.
Enums
Key API / methods
Loan borrow(Member m, String isbn, LocalDate today)Lends the member a copy of the title. It uses the copy held for them if there is one, or else any free copy. It refuses if they are at their loan limit or owe too much. It also refuses if no copy is free for them.
long giveBack(Loan loan, LocalDate today)Closes the loan and charges any late fine. It puts the copy back on the shelf, or keeps it for the first member waiting. It returns the fine. A second call for the same loan returns 0 and changes nothing.
void placeHold(Member m, String isbn)Joins the first-come, first-served queue for a title. It is refused if a copy is on the shelf, since the member can just borrow it.
Code skeleton
import java.time.*;
import java.util.*;
// ---------- Enums ----------
enum CopyStatus { AVAILABLE, ON_LOAN, ON_HOLD_SHELF, LOST }
enum MemberType { STUDENT, FACULTY }
// ---------- A Book is the title; a BookCopy is one physical item on the shelf ----------
final class Book {
final String isbn, title, author;
Book(String isbn, String title, String author) { this.isbn = isbn; this.title = title; this.author = author; }
}
final class BookCopy {
final String barcode; final Book book; CopyStatus status = CopyStatus.AVAILABLE; Member heldFor;
BookCopy(String barcode, Book book) { this.barcode = barcode; this.book = book; }
}
// ---------- Members: limits depend on the type of member ----------
final class Member {
final String id, name; final MemberType type; final List<Loan> activeLoans = new ArrayList<>(); long finesPaise;
final List<String> inbox = new ArrayList<>();
Member(String id, String name, MemberType type) { this.id = id; this.name = name; this.type = type; }
int maxLoans() { return type == MemberType.FACULTY ? 10 : 3; }
int loanDays() { return type == MemberType.FACULTY ? 30 : 14; }
public String toString() { return name; }
}
final class Loan {
final BookCopy copy; final Member member; final LocalDate borrowed, due; LocalDate returned;
Loan(BookCopy copy, Member member, LocalDate borrowed) {
this.copy = copy; this.member = member; this.borrowed = borrowed; this.due = borrowed.plusDays(member.loanDays());
}
}
// ---------- Fines are a policy, so a library can change the rule without touching the service ----------
interface FinePolicy { long fineFor(Loan loan, LocalDate returnedOn); }
final class PerDayFine implements FinePolicy {
private final long perDayPaise, capPaise;
PerDayFine(long perDayPaise, long capPaise) { this.perDayPaise = perDayPaise; this.capPaise = capPaise; }
public long fineFor(Loan loan, LocalDate returnedOn) {
long late = Math.max(0, Duration.between(loan.due.atStartOfDay(), returnedOn.atStartOfDay()).toDays());
return Math.min(capPaise, late * perDayPaise);
}
}
// ---------- Notifications when a held copy comes back ----------
interface Notifier { void notify(Member m, String message); }
final class InboxNotifier implements Notifier { public void notify(Member m, String msg) { m.inbox.add(msg); } }
// ---------- The service: every borrow, return and hold goes through here ----------
final class LibraryService {
private final Map<String, Book> books = new HashMap<>();
private final Map<String, List<BookCopy>> copiesByIsbn = new HashMap<>();
private final Map<String, Deque<Member>> holdQueue = new HashMap<>(); // FIFO per title
private final FinePolicy fines; private final Notifier notifier;
private static final long MAX_UNPAID_FINE = 100_00; // Rs 100 blocks new loans
LibraryService(FinePolicy f, Notifier n) { fines = f; notifier = n; }
synchronized void addCopy(Book b, String barcode) {
books.putIfAbsent(b.isbn, b);
copiesByIsbn.computeIfAbsent(b.isbn, k -> new ArrayList<>()).add(new BookCopy(barcode, b));
}
synchronized Loan borrow(Member m, String isbn, LocalDate today) {
if (m.activeLoans.size() >= m.maxLoans()) throw new IllegalStateException(m + " is at the loan limit of " + m.maxLoans());
if (m.finesPaise >= MAX_UNPAID_FINE) throw new IllegalStateException(m + " has unpaid fines");
BookCopy copy = null;
for (BookCopy c : copiesByIsbn.getOrDefault(isbn, List.of())) {
if (c.status == CopyStatus.ON_HOLD_SHELF && c.heldFor == m) { copy = c; break; } // their reserved copy
if (copy == null && c.status == CopyStatus.AVAILABLE) copy = c;
}
if (copy == null) throw new IllegalStateException("no copy of " + isbn + " is available to " + m);
copy.status = CopyStatus.ON_LOAN; copy.heldFor = null;
holdQueue.getOrDefault(isbn, new ArrayDeque<>()).remove(m);
Loan loan = new Loan(copy, m, today); m.activeLoans.add(loan);
return loan;
}
synchronized long giveBack(Loan loan, LocalDate today) {
if (loan.returned != null) return 0; // returning twice changes nothing
loan.returned = today; loan.member.activeLoans.remove(loan);
long fine = fines.fineFor(loan, today); loan.member.finesPaise += fine;
Deque<Member> q = holdQueue.getOrDefault(loan.copy.book.isbn, new ArrayDeque<>());
Member next = q.pollFirst();
if (next != null) { // first in line gets this copy
loan.copy.status = CopyStatus.ON_HOLD_SHELF; loan.copy.heldFor = next;
notifier.notify(next, "Your hold on '" + loan.copy.book.title + "' is ready");
} else loan.copy.status = CopyStatus.AVAILABLE;
return fine;
}
synchronized void placeHold(Member m, String isbn) {
boolean anyFree = copiesByIsbn.getOrDefault(isbn, List.of()).stream().anyMatch(c -> c.status == CopyStatus.AVAILABLE);
if (anyFree) throw new IllegalStateException("a copy is on the shelf; borrow it instead");
Deque<Member> q = holdQueue.computeIfAbsent(isbn, k -> new ArrayDeque<>());
if (!q.contains(m)) q.addLast(m);
}
synchronized List<Book> searchByTitle(String word) {
String w = word.toLowerCase();
return books.values().stream().filter(b -> b.title.toLowerCase().contains(w)).toList();
}
}
// ---------- Demo: each rule is checked ----------
public class Library {
public static void main(String[] args) {
LibraryService lib = new LibraryService(new PerDayFine(5_00, 50_00), new InboxNotifier()); // Rs 5 a day, capped at Rs 50
Book ddia = new Book("978-1449373320", "Designing Data-Intensive Applications", "Martin Kleppmann");
lib.addCopy(ddia, "C1"); lib.addCopy(ddia, "C2");
Member asha = new Member("m1", "Asha", MemberType.STUDENT), bala = new Member("m2", "Bala", MemberType.STUDENT),
dev = new Member("m3", "Dev", MemberType.FACULTY);
LocalDate d0 = LocalDate.of(2026, 9, 1);
Loan a = lib.borrow(asha, ddia.isbn, d0);
Loan b = lib.borrow(bala, ddia.isbn, d0);
check(!a.copy.barcode.equals(b.copy.barcode), "two members borrow the two different copies");
try { lib.borrow(dev, ddia.isbn, d0); check(false, "no copy left"); }
catch (IllegalStateException e) { check(true, "a third borrow is refused: " + e.getMessage()); }
lib.placeHold(dev, ddia.isbn);
long fineA = lib.giveBack(a, d0.plusDays(14 + 3)); // 3 days late
check(fineA == 15_00, "Asha returns 3 days late: fine Rs 15 at Rs 5 a day");
check(a.copy.status == CopyStatus.ON_HOLD_SHELF && a.copy.heldFor == dev, "the returned copy goes to Dev's hold, not back on the shelf");
check(dev.inbox.size() == 1, "Dev is notified: " + dev.inbox.get(0));
try { lib.borrow(bala, ddia.isbn, d0); check(false, "held copy is not for Bala"); }
catch (IllegalStateException e) { check(true, "the held copy cannot be taken by someone else"); }
Loan dl = lib.borrow(dev, ddia.isbn, d0.plusDays(18));
check(dl.copy == a.copy && dl.due.equals(d0.plusDays(18 + 30)), "Dev collects the held copy; faculty get 30 days");
long fineB = lib.giveBack(b, d0.plusDays(14 + 40)); // 40 days late
check(fineB == 50_00, "40 days late is capped at Rs 50");
check(lib.giveBack(b, d0.plusDays(60)) == 0, "returning the same loan twice changes nothing");
Book sre = new Book("978-1491929124", "Site Reliability Engineering", "Betsy Beyer et al.");
for (int i = 1; i <= 4; i++) lib.addCopy(sre, "S" + i);
Member cara = new Member("m4", "Cara", MemberType.STUDENT);
for (int i = 0; i < 3; i++) lib.borrow(cara, sre.isbn, d0);
try { lib.borrow(cara, sre.isbn, d0); check(false, "limit"); }
catch (IllegalStateException e) { check(true, "a student's 4th loan is refused: " + e.getMessage()); }
check(lib.searchByTitle("reliability").size() == 1, "search by title finds the book");
}
static void check(boolean ok, String what) { System.out.println((ok ? " ok " : " FAIL ") + what); if (!ok) System.exit(1); }
}
/* Output of this exact program (javac + java 21, 2026-09-25):
* ok two members borrow the two different copies
* ok a third borrow is refused: no copy of 978-1449373320 is available to Dev
* ok Asha returns 3 days late: fine Rs 15 at Rs 5 a day
* ok the returned copy goes to Dev's hold, not back on the shelf
* ok Dev is notified: Your hold on 'Designing Data-Intensive Applications' is ready
* ok the held copy cannot be taken by someone else
* ok Dev collects the held copy; faculty get 30 days
* ok 40 days late is capped at Rs 50
* ok returning the same loan twice changes nothing
* ok a student's 4th loan is refused: Cara is at the loan limit of 3
* ok search by title finds the book
*/How it works

The first design choice is the one most candidates miss. A Book is not the thing on the shelf. A Book is a title, with an ISBN, a title and an author. A BookCopy is one physical item with its own barcode and its own state. A library may have five copies of one title. Every loan, return and hold is about one specific copy. Model it this way from the start. If you do not, the design breaks the moment a second copy arrives.
Each copy is in exactly one state: free, on loan, on the hold shelf, or lost. The service is the only thing that changes that state. Every change happens inside one locked method. So two members can never both take the last copy.
Borrowing checks the member first. Are they under their loan limit? That limit depends on whether they are a student or faculty. Are their unpaid fines below the limit? Then it looks for a copy. First it looks for a copy kept on the hold shelf for this member. If there is none, any free copy will do. The loan records the due date, which also depends on the member type.
Returning does three things. It closes the loan. It asks the FinePolicy for a fine. And it decides where the copy goes. If members are waiting for that title, the copy is kept on the hold shelf for the first of them. That member is told. If nobody is waiting, it goes back on the shelf. The fine rule is a Strategy. So a new rule, like a fixed late fee or a grace period, is a new class. The service does not change. Telling members uses the Observer idea. So the same code can send an app message, an email or an SMS.
The program checks every one of these rules. Two members get two different copies. A third member is refused. A return 3 days late costs ₹15, at ₹5 a day. The returned copy goes to the waiting member, who is told. Nobody else can take it. A return 40 days late is capped at ₹50. Returning twice changes nothing. And a student's fourth loan is refused.
Some natural next steps. Renewals are allowed only if nobody is waiting. A held copy has a pickup deadline. After that, it passes to the next person in line. And a member can report a copy as lost.
Edge cases & gotchas
- All copies are out. Borrowing is refused, and the member can place a hold instead.
- A copy comes back while members are waiting. It goes to the hold shelf for the first in the queue, not back on the open shelf.
- Someone else tries to take a copy kept for a hold. It is refused, because the copy is marked for one member.
- A book comes back very late. The fine has a cap, here ₹50. So a lost book that is later found does not get a silly fine.
- The same loan is returned twice, for example from a double scan at the desk. The second return changes nothing.
- A member at their loan limit cannot borrow until they return a book. The same goes for too many unpaid fines, until they pay.
- A member who places a hold twice for the same title stays in the queue only once.