Library Management is the classic CRUD-meets-business-rules LLD question. It sounds simple at first — add books, search, borrow, return. But then the interviewer asks about multiple copies, fines, reservations, and suddenly there’s real design thinking needed.
The key insight that makes or breaks this design: a Book is not the same as a BookItem. “Clean Code” is a Book. The three physical copies on the shelf are BookItems. Get this right and everything else falls into place.
Requirements
Functional:
- Add, remove, and search books (by title, author, or ISBN)
- Each book can have multiple physical copies (BookItems)
- Members can borrow and return books
- Maximum 5 books per member at a time
- Loan period is 14 days. Late returns incur a fine ($1/day)
- Members can reserve a book that’s currently loaned out
- Notify members when their reserved book becomes available
Constraints:
- Members must have an active account to borrow
- A BookItem can only be loaned to one member at a time
- Librarians can add/remove books and manage members
Key Classes & Relationships
- members: Map<id, Member>
+ searchByTitle(title)
+ borrowBook(memberId, isbn)
+ returnBook(memberId, itemId)
- title: string
- author: string
- items: BookItem[]
- name: string
- activeLoans: Loan[]
- totalFines: float
- status: BookStatus
- isbn: string (→ Book)
- member: Member
- borrowDate: Date
- dueDate: Date
- returnDate: Date?
The Book vs BookItem distinction is the most important modeling decision. A Book is the catalog entry (metadata). A BookItem is a physical copy that can be loaned, returned, lost. One Book can have 5 BookItems, each with its own status.
Core Implementation
from enum import Enum
from datetime import datetime, timedelta
class BookStatus(Enum):
AVAILABLE = "available"
LOANED = "loaned"
RESERVED = "reserved"
LOST = "lost"
# --- Core Models ---
class BookItem:
def __init__(self, barcode: str, isbn: str):
self.barcode = barcode
self.isbn = isbn
self.status = BookStatus.AVAILABLE
class Book:
def __init__(self, isbn: str, title: str, author: str):
self.isbn = isbn
self.title = title
self.author = author
self.items: list[BookItem] = []
def add_item(self, barcode: str) -> BookItem:
item = BookItem(barcode, self.isbn)
self.items.append(item)
return item
def get_available_item(self) -> BookItem | None:
for item in self.items:
if item.status == BookStatus.AVAILABLE:
return item
return None
class Loan:
LOAN_DAYS = 14
FINE_PER_DAY = 1.0
def __init__(self, item: BookItem, member_id: str):
self.item = item
self.member_id = member_id
self.borrow_date = datetime.now()
self.due_date = self.borrow_date + timedelta(days=self.LOAN_DAYS)
self.return_date: datetime | None = None
def calculate_fine(self) -> float:
end = self.return_date or datetime.now()
if end <= self.due_date:
return 0.0
overdue_days = (end - self.due_date).days
return overdue_days * self.FINE_PER_DAY
class Member:
MAX_LOANS = 5
def __init__(self, member_id: str, name: str):
self.member_id = member_id
self.name = name
self.active_loans: list[Loan] = []
self.total_fines = 0.0
def can_borrow(self) -> bool:
return len(self.active_loans) < self.MAX_LOANS
# --- Library (Facade) ---
class Library:
def __init__(self):
self.books: dict[str, Book] = {} # isbn → Book
self.members: dict[str, Member] = {} # id → Member
self.observers: dict[str, list[str]] = {} # isbn → [member_ids waiting]
def add_book(self, isbn: str, title: str, author: str) -> Book:
if isbn not in self.books:
self.books[isbn] = Book(isbn, title, author)
return self.books[isbn]
def add_member(self, member_id: str, name: str) -> Member:
member = Member(member_id, name)
self.members[member_id] = member
return member
def search_by_title(self, query: str) -> list[Book]:
query = query.lower()
return [b for b in self.books.values() if query in b.title.lower()]
def search_by_author(self, query: str) -> list[Book]:
query = query.lower()
return [b for b in self.books.values() if query in b.author.lower()]
def borrow_book(self, member_id: str, isbn: str) -> Loan | None:
member = self.members.get(member_id)
book = self.books.get(isbn)
if not member or not book:
print("Member or book not found.")
return None
if not member.can_borrow():
print(f"{member.name} has reached max loans ({Member.MAX_LOANS}).")
return None
item = book.get_available_item()
if not item:
print(f"No copies available for '{book.title}'.")
return None
item.status = BookStatus.LOANED
loan = Loan(item, member_id)
member.active_loans.append(loan)
print(f"{member.name} borrowed '{book.title}' (due: {loan.due_date.date()})")
return loan
def return_book(self, member_id: str, barcode: str) -> float:
member = self.members.get(member_id)
if not member:
print("Member not found.")
return 0.0
loan = next((l for l in member.active_loans if l.item.barcode == barcode), None)
if not loan:
print("No active loan found for this item.")
return 0.0
loan.return_date = datetime.now()
fine = loan.calculate_fine()
member.total_fines += fine
loan.item.status = BookStatus.AVAILABLE
member.active_loans.remove(loan)
isbn = loan.item.isbn
print(f"Returned '{self.books[isbn].title}'. Fine: ${fine:.2f}")
# Notify waiting members
self._notify_reservation(isbn)
return fine
def reserve_book(self, member_id: str, isbn: str):
if isbn not in self.observers:
self.observers[isbn] = []
self.observers[isbn].append(member_id)
print(f"Member {member_id} added to waitlist for ISBN {isbn}")
def _notify_reservation(self, isbn: str):
if isbn in self.observers and self.observers[isbn]:
book = self.books[isbn]
if book.get_available_item():
next_member_id = self.observers[isbn].pop(0)
member = self.members.get(next_member_id)
if member:
print(f" Notification: {member.name}, '{book.title}' is now available!")
# Usage
lib = Library()
book = lib.add_book("978-0132350884", "Clean Code", "Robert C. Martin")
book.add_item("CC-001")
book.add_item("CC-002")
alice = lib.add_member("M001", "Alice")
bob = lib.add_member("M002", "Bob")
lib.borrow_book("M001", "978-0132350884") # Alice borrows copy 1
lib.borrow_book("M002", "978-0132350884") # Bob borrows copy 2
lib.return_book("M001", "CC-001") # Alice returns
const BookStatus = Object.freeze({
AVAILABLE: "available",
LOANED: "loaned",
RESERVED: "reserved",
LOST: "lost",
});
class BookItem {
constructor(barcode, isbn) {
this.barcode = barcode;
this.isbn = isbn;
this.status = BookStatus.AVAILABLE;
}
}
class Book {
constructor(isbn, title, author) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.items = [];
}
addItem(barcode) {
const item = new BookItem(barcode, this.isbn);
this.items.push(item);
return item;
}
getAvailableItem() {
return this.items.find((i) => i.status === BookStatus.AVAILABLE) || null;
}
}
class Loan {
static LOAN_DAYS = 14;
static FINE_PER_DAY = 1.0;
constructor(item, memberId) {
this.item = item;
this.memberId = memberId;
this.borrowDate = new Date();
this.dueDate = new Date(Date.now() + Loan.LOAN_DAYS * 86400000);
this.returnDate = null;
}
calculateFine() {
const end = this.returnDate || new Date();
if (end <= this.dueDate) return 0;
const overdueDays = Math.ceil((end - this.dueDate) / 86400000);
return overdueDays * Loan.FINE_PER_DAY;
}
}
class Member {
static MAX_LOANS = 5;
constructor(id, name) {
this.id = id;
this.name = name;
this.activeLoans = [];
this.totalFines = 0;
}
canBorrow() { return this.activeLoans.length < Member.MAX_LOANS; }
}
class Library {
constructor() {
this.books = new Map(); // isbn → Book
this.members = new Map(); // id → Member
this.waitlists = new Map(); // isbn → [memberIds]
}
addBook(isbn, title, author) {
if (!this.books.has(isbn)) this.books.set(isbn, new Book(isbn, title, author));
return this.books.get(isbn);
}
addMember(id, name) {
const m = new Member(id, name);
this.members.set(id, m);
return m;
}
searchByTitle(query) {
const q = query.toLowerCase();
return [...this.books.values()].filter((b) => b.title.toLowerCase().includes(q));
}
borrowBook(memberId, isbn) {
const member = this.members.get(memberId);
const book = this.books.get(isbn);
if (!member || !book) return null;
if (!member.canBorrow()) { console.log("Max loans reached."); return null; }
const item = book.getAvailableItem();
if (!item) { console.log(`No copies of '${book.title}' available.`); return null; }
item.status = BookStatus.LOANED;
const loan = new Loan(item, memberId);
member.activeLoans.push(loan);
console.log(`${member.name} borrowed '${book.title}'`);
return loan;
}
returnBook(memberId, barcode) {
const member = this.members.get(memberId);
if (!member) return 0;
const idx = member.activeLoans.findIndex((l) => l.item.barcode === barcode);
if (idx === -1) return 0;
const loan = member.activeLoans.splice(idx, 1)[0];
loan.returnDate = new Date();
const fine = loan.calculateFine();
member.totalFines += fine;
loan.item.status = BookStatus.AVAILABLE;
const book = this.books.get(loan.item.isbn);
console.log(`Returned '${book.title}'. Fine: $${fine.toFixed(2)}`);
this._notifyWaitlist(loan.item.isbn);
return fine;
}
_notifyWaitlist(isbn) {
const list = this.waitlists.get(isbn);
if (!list || list.length === 0) return;
const book = this.books.get(isbn);
if (book.getAvailableItem()) {
const nextId = list.shift();
const m = this.members.get(nextId);
if (m) console.log(` Notification: ${m.name}, '${book.title}' is available!`);
}
}
}
// Usage
const lib = new Library();
const book = lib.addBook("978-0132350884", "Clean Code", "Robert C. Martin");
book.addItem("CC-001");
lib.addMember("M001", "Alice");
lib.borrowBook("M001", "978-0132350884");
lib.returnBook("M001", "CC-001");
import java.util.*;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
enum BookStatus { AVAILABLE, LOANED, RESERVED, LOST }
class BookItem {
String barcode, isbn;
BookStatus status = BookStatus.AVAILABLE;
BookItem(String barcode, String isbn) { this.barcode = barcode; this.isbn = isbn; }
}
class Book {
String isbn, title, author;
List<BookItem> items = new ArrayList<>();
Book(String isbn, String title, String author) {
this.isbn = isbn; this.title = title; this.author = author;
}
BookItem addItem(String barcode) {
BookItem item = new BookItem(barcode, isbn);
items.add(item);
return item;
}
BookItem getAvailableItem() {
return items.stream().filter(i -> i.status == BookStatus.AVAILABLE)
.findFirst().orElse(null);
}
}
class Loan {
static final int LOAN_DAYS = 14;
static final double FINE_PER_DAY = 1.0;
BookItem item;
String memberId;
LocalDate borrowDate, dueDate;
LocalDate returnDate;
Loan(BookItem item, String memberId) {
this.item = item;
this.memberId = memberId;
this.borrowDate = LocalDate.now();
this.dueDate = borrowDate.plusDays(LOAN_DAYS);
}
double calculateFine() {
LocalDate end = returnDate != null ? returnDate : LocalDate.now();
if (!end.isAfter(dueDate)) return 0;
long overdue = ChronoUnit.DAYS.between(dueDate, end);
return overdue * FINE_PER_DAY;
}
}
class Member {
static final int MAX_LOANS = 5;
String id, name;
List<Loan> activeLoans = new ArrayList<>();
double totalFines = 0;
Member(String id, String name) { this.id = id; this.name = name; }
boolean canBorrow() { return activeLoans.size() < MAX_LOANS; }
}
class Library {
Map<String, Book> books = new HashMap<>();
Map<String, Member> members = new HashMap<>();
Map<String, Queue<String>> waitlists = new HashMap<>();
Book addBook(String isbn, String title, String author) {
books.putIfAbsent(isbn, new Book(isbn, title, author));
return books.get(isbn);
}
Member addMember(String id, String name) {
Member m = new Member(id, name);
members.put(id, m);
return m;
}
Loan borrowBook(String memberId, String isbn) {
Member member = members.get(memberId);
Book book = books.get(isbn);
if (member == null || book == null) return null;
if (!member.canBorrow()) return null;
BookItem item = book.getAvailableItem();
if (item == null) {
System.out.println("No copies available.");
return null;
}
item.status = BookStatus.LOANED;
Loan loan = new Loan(item, memberId);
member.activeLoans.add(loan);
System.out.println(member.name + " borrowed '" + book.title + "'");
return loan;
}
double returnBook(String memberId, String barcode) {
Member member = members.get(memberId);
if (member == null) return 0;
Loan loan = member.activeLoans.stream()
.filter(l -> l.item.barcode.equals(barcode))
.findFirst().orElse(null);
if (loan == null) return 0;
loan.returnDate = LocalDate.now();
double fine = loan.calculateFine();
member.totalFines += fine;
loan.item.status = BookStatus.AVAILABLE;
member.activeLoans.remove(loan);
System.out.println("Returned. Fine: $" + fine);
return fine;
}
}
Design Patterns Used
Facade — The Library class is a facade over the entire system. Members don’t interact with BookItem or Loan directly. They call library.borrowBook() and the library handles all the internal coordination — finding available copies, creating loans, updating statuses.
Observer — The reservation/waitlist system. When a book is returned, the library checks if anyone is waiting and notifies them. We store a list of member IDs per ISBN. When a copy becomes available, we pop the first waiter and notify them.
Composition — Book HAS BookItems. Member HAS Loans. Library HAS Books and Members. Everything is connected through references, not inheritance.
Value Objects — Loan captures a point-in-time transaction. It has a borrow date, due date, and optional return date. The fine calculation is derived from these dates. Clean and testable.
Extensions
“Add a reservation system” — When no copies are available, a member can reserve. We already have the waitlist. Extend it so the next available copy gets status RESERVED and is held for 24 hours.
“Support renewal” — Add a renewLoan(memberId, barcode) method. Extend the due date by another 14 days. But only if: no one has reserved it, and the member hasn’t already renewed once.
“Different member tiers” — Gold members get 10 books, Silver gets 7, Basic gets 5. Use a MemberTier enum and look up the limit. Or use Strategy pattern where each tier defines its own borrowing rules.
“Support digital books (e-books)” — This is where we might introduce an interface like Borrowable that both BookItem and DigitalBook implement. Digital books don’t have a barcode or physical status, but they still have loan limits.
“Search with multiple filters” — Add a SearchCriteria builder: search().byTitle("clean").byAuthor("martin").available().execute(). This is a nice spot for the Builder pattern.
In simple language, the Library question is really about modeling the right entities. The moment we separate Book (catalog entry) from BookItem (physical copy), everything clicks. One Book, many copies. Each copy has its own lifecycle. The Library facade ties it all together so nobody has to juggle the internals.