Low Level Design
Design a Library Management System
A full low-level design for a library — rack-based book storage, dual borrow flows (by book ID or copy ID), multi-attribute search, borrow-log tracking, and a thin facade that separates the public API from the core engine.
Problem Description#
Design a Library Management System that automates cataloguing, checkout, return, and search operations for a physical library.
A library is a richer domain than it first appears. Multiple copies of the same book exist and must be tracked independently. Racks enforce physical placement rules — one book title per rack, so the same book can't appear twice in the same rack. Borrowing a book can happen two ways: grab any available copy of a given title, or reserve a specific copy by ID. Every borrow action must be logged with dates, and users face hard limits on how many books they may hold.
This is a classic hard LLD interview problem: it involves a rich domain model, multiple interacting entities, two distinct borrow flows that share common steps (Template Method), pluggable search (Strategy), a facade layer, and careful attention to invariant enforcement throughout.
Clarify Requirements#
Before designing, ask these questions in an interview:
Functional
- How many copies of the same book can exist, and can the same title appear on the same rack?
- Can a user borrow by book title/ID (any copy) or must they always specify a copy?
- What is the borrow period, and is there a late-fee model?
- What happens when a user tries to borrow beyond their limit?
- What happens when all copies of a book are borrowed and a user tries to borrow it?
- Which attributes should search support — ID, title, author, publisher?
- When a book copy is returned, where does it go back on the shelf?
Non-functional
- Should concurrent borrow requests be handled safely?
- Does the borrow log need to be persisted or is in-memory sufficient?
- Is the CLI interaction model fixed, or will a REST API be added later?
- Should removing the last copy of a book auto-remove the book record?
Final Requirements#
After clarification, here's what we'll build:
- A library is initialised with N racks; each rack holds at most one copy of any given book title
- addBook creates a Book record and places each BookCopy into the first available rack (ascending rack order)
- borrowBook(bookId) finds the lowest-rack available copy of a book and marks it borrowed; borrowBookCopy(copyId) borrows a specific copy — both share common validation steps
- Due date is set to 14 days from the borrow date; every borrow action creates a LibraryBorrowedLog entry
- Users have a borrow limit of 5 books; attempts beyond the limit are rejected
- returnBookCopy clears borrow details, marks the log, and places the copy in the first available rack
- searchBooks supports four attributes: book_id, title, author, publisher
- LibraryManagementInterface is a thin facade that parses string inputs and delegates to the core LibraryManagementSystem engine
- Removing the last copy of a book automatically removes the book record
Core Entities#
| Entity | Responsibility |
|---|---|
| Book | Immutable catalogue record — ID, title, authors, publishers, copy ID list |
| BookCopy | Extends Book; adds copy-specific state — copy ID, rack number, borrower, due date |
| Rack | Physical shelf unit — holds a list of BookCopy objects; prevents the same book title appearing twice via a Set<String> of book IDs |
| LibraryUser | User record — ID, name, borrow limit, list of currently borrowed copies |
| LibraryBorrowedLog | Audit record per borrow — borrow date, due date, return date (null until returned) |
| LibraryManagementSystem | Core engine — owns all maps and racks; implements every operation; enforces all invariants |
| LibraryManagementInterface | Public facade — parses comma-separated string params, delegates to the engine |
| LibraryManagementSystemSimulation | CLI entry point — reads commands from stdin, dispatches to the facade |
Patterns Used#
1. Facade — LibraryManagementInterface#
The facade absorbs input parsing (splitting comma-separated strings into List<String>) and exposes a friendly API. External callers — the CLI simulation, a future REST controller — never touch LibraryManagementSystem directly. This keeps the engine's API strongly typed while the facade handles the messy string world.
LibraryManagementSystemSimulation
└─ LibraryManagementInterface ← facade: parses strings, delegates
└─ LibraryManagementSystem ← engine: all business logic
2. Template Method — dual borrow flows#
borrowBook(bookId) and borrowBookCopy(copyId) share the same structure:
- Validate the user (exists? within borrow limit?)
- Locate the target copy
- Compute due date, create a LibraryBorrowedLog, mark the copy borrowed, add to user's list
Step 2 is the only variation — one searches by book ID across racks, the other looks up a copy directly by ID. Both paths converge on the same steps 3–4, avoiding duplication.
3. Strategy — searchBooks by attribute#
searchBooks(attribute, value) dispatches to one of four private methods based on the attribute string. Each method (searchByBookId, searchByTitle, searchByAuthor, searchByPublisher) applies its own matching logic against copyIdToBookCopy. Adding a new search dimension (e.g., isbn) means adding one private method and one case — the public signature never changes.
4. Factory-like construction — addBook#
LibraryManagementSystem.addBook() is responsible for creating BookCopy instances from the raw copy-ID list. Callers never instantiate BookCopy directly; the engine controls object creation, assigns rack numbers, and registers copies in both the rack and the copy map.
5. Composition — Library owns everything#
LibraryManagementSystem holds four maps and a log list. Rack holds a list of BookCopy objects plus a duplicate-prevention set. LibraryUser holds a list of borrowed copies. No entity reaches up into the library to mutate global state — the engine orchestrates all cross-entity operations.
Code#
Domain models — Book and BookCopy#
Book is an immutable catalogue record. BookCopy extends it and adds mutable borrow state — the copy's current rack number, who borrowed it, and when it's due.
import java.util.List;
public class Book {
private final String bookId;
private final String title;
private final List<String> authors;
private final List<String> publishingCompany;
private final List<String> bookCopyIds;
public Book(String bookId, String title, List<String> authors,
List<String> publishingCompany, List<String> bookCopyIds) {
this.bookId = bookId;
this.title = title;
this.authors = authors;
this.publishingCompany = publishingCompany;
this.bookCopyIds = bookCopyIds;
}
public String getBookId() { return bookId; }
public String getTitle() { return title; }
public List<String> getAuthors() { return authors; }
public List<String> getPublishingCompany() { return publishingCompany; }
public List<String> getBookCopyIds() { return bookCopyIds; }
}import java.util.Date;
public class BookCopy extends Book {
private final String copyId;
private final Book book;
private Integer rackNumber;
private LibraryUser borrowedBy;
private Date dueDate;
public BookCopy(String copyId, Book book, Integer rackNumber) {
super(book.getBookId(), book.getTitle(), book.getAuthors(),
book.getPublishingCompany(), book.getBookCopyIds());
this.copyId = copyId;
this.book = book;
this.rackNumber = rackNumber;
this.borrowedBy = null;
this.dueDate = null;
}
public String getCopyId() { return copyId; }
public Book getBookDetails() { return book; }
public Book getBook() { return book; }
public Integer getRackNumber() { return rackNumber; }
public LibraryUser getBorrowedBy(){ return borrowedBy; }
public Date getDueDate() { return dueDate; }
public void setRackNumber(Integer rackNumber) { this.rackNumber = rackNumber; }
public void setBorrowedDetails(LibraryUser user, Date dueDate) {
this.borrowedBy = user;
this.dueDate = dueDate;
}
public boolean isAvailable() {
return this.borrowedBy == null;
}
}Supporting models — User, Rack, BorrowedLog#
LibraryUser guards the borrow limit in addBorrowedBook(). Rack prevents duplicate book titles with a Set<String> of book IDs. LibraryBorrowedLog is the audit trail — one entry per borrow transaction, updated when the book is returned.
import java.util.ArrayList;
import java.util.List;
public class LibraryUser {
private final String userId;
private final String name;
private final Integer borrowLimit;
private final List<BookCopy> borrowedBooks;
public LibraryUser(String userId, String name, Integer borrowLimit) {
this.userId = userId;
this.name = name;
this.borrowLimit = borrowLimit;
this.borrowedBooks = new ArrayList<>();
}
public String getUserId() { return userId; }
public String getName() { return name; }
public Integer getBorrowLimit() { return borrowLimit; }
public List<BookCopy> getBorrowedBooks(){ return borrowedBooks; }
public void addBorrowedBook(BookCopy bookCopy) {
if (borrowedBooks.size() < borrowLimit) {
borrowedBooks.add(bookCopy);
} else {
System.out.println("[ERROR] | User " + name + " has reached the borrow limit.");
}
}
}import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Rack {
private final Integer rackNumber;
private final List<BookCopy> bookCopyList;
private final Set<String> bookIdsInRack; // prevents same title appearing twice
public Rack(Integer rackNumber) {
this.rackNumber = rackNumber;
this.bookCopyList = new ArrayList<>();
this.bookIdsInRack = new HashSet<>();
}
public Integer getRackNumber() { return rackNumber; }
public List<BookCopy> getBookCopyList() { return bookCopyList; }
public void addBookCopy(BookCopy bookCopy) {
if (checkAvailabilityForBook(bookCopy)) {
bookCopyList.add(bookCopy);
bookIdsInRack.add(bookCopy.getBookId());
} else {
System.out.println("[WARN] | Skipping duplicate book ID: " + bookCopy.getBookId());
}
}
public void removeBookCopy(String copyId) {
BookCopy toRemove = null;
for (BookCopy copy : bookCopyList) {
if (copy.getCopyId().equals(copyId)) {
toRemove = copy;
break;
}
}
if (toRemove != null) {
bookCopyList.remove(toRemove);
bookIdsInRack.remove(toRemove.getBookId());
} else {
System.out.println("[WARN] | Book copy ID " + copyId + " not found in rack " + rackNumber);
}
}
public boolean checkAvailabilityForBook(BookCopy bookCopy) {
return !bookIdsInRack.contains(bookCopy.getBookId());
}
public void displayBookCopies() {
System.out.println("Rack Number: " + rackNumber);
if (bookCopyList.isEmpty()) {
System.out.println(" - No book copies in this rack.");
return;
}
for (BookCopy copy : bookCopyList) {
System.out.println(" - Copy ID: " + copy.getCopyId() + ", Title: " + copy.getTitle());
}
}
}import java.util.Date;
public class LibraryBorrowedLog {
private final String logId;
private final BookCopy bookCopy;
private final LibraryUser user;
private final Date borrowDate;
private final Date dueDate;
private Date returnDate;
public LibraryBorrowedLog(String logId, BookCopy bookCopy, LibraryUser user,
Date borrowDate, Date dueDate) {
this.logId = logId;
this.bookCopy = bookCopy;
this.user = user;
this.borrowDate = borrowDate;
this.dueDate = dueDate;
this.returnDate = null;
}
public String getLogId() { return logId; }
public BookCopy getBookCopy() { return bookCopy; }
public LibraryUser getUser() { return user; }
public Date getBorrowDate() { return borrowDate; }
public Date getDueDate() { return dueDate; }
public Date getReturnDate() { return returnDate; }
public void markAsReturned(Date returnDate) {
this.returnDate = returnDate;
}
}Core engine — LibraryManagementSystem#
The engine owns all data structures and enforces every invariant. addBook places copies into the lowest-numbered available racks. Both borrow flows validate the user and log the transaction before marking the copy. returnBookCopy clears borrow state and re-shelves the copy. searchBooks dispatches to the right private search method based on the attribute key.
import java.util.*;
public class LibraryManagementSystem {
private final String libraryId;
private final Integer borrowLimitPerUser = 5;
private final Integer totalRacks;
private final Map<Integer, Rack> racks; // rackNumber → Rack
private final Map<String, BookCopy> copyIdToBookCopy; // copyId → BookCopy
private final Map<String, LibraryUser> userIdToLibraryUser;
private final Map<String, Book> bookIdToBook;
private final List<LibraryBorrowedLog> borrowedLogs;
public LibraryManagementSystem(String libraryId, Integer totalRacks) {
this.libraryId = libraryId;
this.totalRacks = totalRacks;
this.racks = new HashMap<>();
for (int i = 1; i <= totalRacks; i++) {
racks.put(i, new Rack(i));
}
this.copyIdToBookCopy = new HashMap<>();
this.userIdToLibraryUser = new HashMap<>();
this.bookIdToBook = new HashMap<>();
this.borrowedLogs = new ArrayList<>();
}
public Integer getBorrowLimitPerUser() { return borrowLimitPerUser; }
// ── Add / Remove ─────────────────────────────────────────────────────────
public void addBook(String bookId, String title, List<String> authorList,
List<String> publishingCompanyList, List<String> bookCopyIdList) {
Book book = new Book(bookId, title, authorList, publishingCompanyList, bookCopyIdList);
// Verify enough racks are available (one per copy)
int availableRackCount = 0;
for (Rack rack : racks.values()) {
if (rack.checkAvailabilityForBook(new BookCopy("temp", book, rack.getRackNumber()))) {
availableRackCount++;
}
}
if (availableRackCount < bookCopyIdList.size()) {
System.out.println("[ERROR] | Not enough racks for book ID: " + bookId);
return;
}
List<Integer> rackNumbers = new ArrayList<>();
for (String copyId : bookCopyIdList) {
for (int i = 1; i <= totalRacks; i++) {
Rack rack = racks.get(i);
BookCopy newBookCopy = new BookCopy(copyId, book, rack.getRackNumber());
if (rack.checkAvailabilityForBook(newBookCopy)) {
rack.addBookCopy(newBookCopy);
copyIdToBookCopy.put(copyId, newBookCopy);
rackNumbers.add(rack.getRackNumber());
break;
}
}
}
bookIdToBook.put(bookId, book);
System.out.println("[INFO] | Added book to racks: " + rackNumbers);
}
public void removeBookCopy(String copyId) {
BookCopy bookCopy = copyIdToBookCopy.get(copyId);
if (bookCopy == null) {
System.out.println("[ERROR] | Invalid Book Copy ID");
return;
}
Rack rack = racks.get(bookCopy.getRackNumber());
if (rack != null) {
rack.removeBookCopy(copyId);
copyIdToBookCopy.remove(copyId);
System.out.println("[INFO] | Removed book copy: " + copyId + " from rack: " + rack.getRackNumber());
boolean anyCopyLeft = copyIdToBookCopy.values().stream()
.anyMatch(c -> c.getBookId().equals(bookCopy.getBookId()));
if (!anyCopyLeft) {
bookIdToBook.remove(bookCopy.getBookId());
System.out.println("[INFO] | No copies left for Book ID: " + bookCopy.getBookId() + ". Book removed.");
}
}
}
public void addUser(String userId, String name) {
LibraryUser user = new LibraryUser(userId, name, borrowLimitPerUser);
userIdToLibraryUser.put(userId, user);
System.out.println("[INFO] | Added user: " + name + " with User ID: " + userId);
}
// ── Borrow ───────────────────────────────────────────────────────────────
/** Borrow any available copy of a book by book ID (lowest rack preferred). */
public void borrowBook(String bookId, String userId, Date borrowDate) {
LibraryUser user = userIdToLibraryUser.get(userId);
if (!verifyUser(user, true)) return;
if (!bookIdToBook.containsKey(bookId)) {
System.out.println("[ERROR] | Invalid Book ID");
return;
}
BookCopy availableCopy = null;
outer:
for (int i = 1; i <= totalRacks; i++) {
for (BookCopy copy : racks.get(i).getBookCopyList()) {
if (copy.getBookId().equals(bookId) && copy.isAvailable()) {
availableCopy = copy;
break outer;
}
}
}
if (availableCopy == null) {
System.out.println("[ERROR] | No available copies for Book ID: " + bookId);
return;
}
completeBorrow(availableCopy, user, borrowDate);
}
/** Borrow a specific copy by its copy ID. */
public void borrowBookCopy(String bookCopyId, String userId, Date borrowDate) {
LibraryUser user = userIdToLibraryUser.get(userId);
if (!verifyUser(user, true)) return;
BookCopy bookCopy = copyIdToBookCopy.get(bookCopyId);
if (bookCopy == null) {
System.out.println("[ERROR] | Invalid Book Copy ID");
return;
}
if (!bookCopy.isAvailable()) {
System.out.println("[ERROR] | Book Copy ID: " + bookCopyId + " is not available");
return;
}
completeBorrow(bookCopy, user, borrowDate);
}
/** Shared steps for both borrow flows: log, mark copy, update user. */
private void completeBorrow(BookCopy copy, LibraryUser user, Date borrowDate) {
Date dueDate = new Date(borrowDate.getTime() + (14L * 24 * 60 * 60 * 1000));
LibraryBorrowedLog log = new LibraryBorrowedLog(
UUID.randomUUID().toString(), copy, user, borrowDate, dueDate);
borrowedLogs.add(log);
copy.setBorrowedDetails(user, dueDate);
user.addBorrowedBook(copy);
System.out.println("[INFO] | " + user.getName() + " borrowed copy ID: "
+ copy.getCopyId() + ", Due: " + dueDate + ", Rack: " + copy.getRackNumber());
}
// ── Return ───────────────────────────────────────────────────────────────
public void returnBookCopy(String bookCopyId) {
BookCopy bookCopy = copyIdToBookCopy.get(bookCopyId);
if (bookCopy == null) {
System.out.println("[ERROR] | Invalid Book Copy ID");
return;
}
for (LibraryBorrowedLog log : borrowedLogs) {
if (log.getBookCopy().getCopyId().equals(bookCopyId) && log.getReturnDate() == null) {
log.markAsReturned(new Date());
break;
}
}
LibraryUser user = bookCopy.getBorrowedBy();
bookCopy.setBorrowedDetails(null, null);
if (user != null) user.getBorrowedBooks().remove(bookCopy);
returnBookCopyToRack(bookCopy);
System.out.println("[INFO] | Copy ID: " + bookCopyId + " returned to rack: " + bookCopy.getRackNumber());
}
private void returnBookCopyToRack(BookCopy bookCopy) {
for (int i = 1; i <= totalRacks; i++) {
Rack rack = racks.get(i);
if (rack.checkAvailabilityForBook(bookCopy)) {
rack.addBookCopy(bookCopy);
bookCopy.setRackNumber(i);
return;
}
}
System.out.println("[WARN] | No rack available for returned copy: " + bookCopy.getCopyId());
}
// ── Query ────────────────────────────────────────────────────────────────
public void printBorrowedBooks(String userId) {
if (!verifyUser(userIdToLibraryUser.get(userId))) return;
LibraryUser user = userIdToLibraryUser.get(userId);
List<BookCopy> borrowedBooks = user.getBorrowedBooks();
if (borrowedBooks.isEmpty()) {
System.out.println("[INFO] | " + user.getName() + " has no borrowed books.");
return;
}
System.out.println("[INFO] | " + user.getName() + " borrowed:");
for (BookCopy copy : borrowedBooks) {
System.out.println("[DETAILS] | Copy: " + copy.getCopyId()
+ " Title: " + copy.getTitle() + " Due: " + copy.getDueDate());
}
}
public void searchBooks(String attribute, String value) {
if (attribute == null || value == null || value.trim().isEmpty()) {
System.out.println("[ERROR] | Provide a valid attribute and value");
return;
}
switch (attribute) {
case "book_id" -> searchByBookId(value);
case "title" -> searchByTitle(value);
case "author" -> searchByAuthor(value);
case "publisher" -> searchByPublisher(value);
default -> System.out.println("[ERROR] | Allowed attributes: book_id, title, author, publisher");
}
}
private void searchByBookId(String bookId) {
boolean found = false;
for (BookCopy copy : copyIdToBookCopy.values()) {
if (copy.getBookId().equalsIgnoreCase(bookId)) { printBookCopyDetails(copy); found = true; }
}
if (!found) System.out.println("[ERROR] | No books found for Book ID: " + bookId);
}
private void searchByTitle(String title) {
boolean found = false;
for (BookCopy copy : copyIdToBookCopy.values()) {
if (copy.getTitle().equalsIgnoreCase(title)) { printBookCopyDetails(copy); found = true; }
}
if (!found) System.out.println("[ERROR] | No books found for title: " + title);
}
private void searchByAuthor(String author) {
boolean found = false;
for (BookCopy copy : copyIdToBookCopy.values()) {
if (copy.getAuthors() != null && copy.getAuthors().stream()
.anyMatch(a -> a.equalsIgnoreCase(author))) {
printBookCopyDetails(copy); found = true;
}
}
if (!found) System.out.println("[ERROR] | No books found for author: " + author);
}
private void searchByPublisher(String publisher) {
boolean found = false;
for (BookCopy copy : copyIdToBookCopy.values()) {
if (copy.getPublishingCompany() != null && copy.getPublishingCompany().stream()
.anyMatch(p -> p.equalsIgnoreCase(publisher))) {
printBookCopyDetails(copy); found = true;
}
}
if (!found) System.out.println("[ERROR] | No books found for publisher: " + publisher);
}
private void printBookCopyDetails(BookCopy copy) {
Book book = copy.getBook();
String authors = String.join(", ", book.getAuthors());
String pubs = String.join(", ", book.getPublishingCompany());
String borrower = copy.getBorrowedBy() != null ? copy.getBorrowedBy().getUserId() : "N/A";
String dueDate = "N/A";
for (LibraryBorrowedLog log : borrowedLogs) {
if (log.getBookCopy().equals(copy) && log.getReturnDate() == null) {
dueDate = log.getDueDate().toString(); break;
}
}
System.out.printf("Copy: %s | Book: %s | Title: %s | Authors: %s | Publishers: %s | Rack: %d | BorrowedBy: %s | Due: %s%n",
copy.getCopyId(), book.getBookId(), book.getTitle(), authors, pubs,
copy.getRackNumber(), borrower, dueDate);
}
public void displayRacks() {
System.out.println("Current rack state:");
for (Rack rack : racks.values()) rack.displayBookCopies();
}
// ── Validation helpers ───────────────────────────────────────────────────
private boolean verifyUser(LibraryUser user) { return verifyUser(user, false); }
private boolean verifyUser(LibraryUser user, boolean checkLimit) {
if (user == null) { System.out.println("[ERROR] | Invalid User ID"); return false; }
if (checkLimit && user.getBorrowedBooks().size() >= borrowLimitPerUser) {
System.out.println("[ERROR] | User has reached the borrow limit"); return false;
}
return true;
}
}
Facade and CLI simulation#
LibraryManagementInterface parses comma-delimited strings into lists, then delegates. LibraryManagementSystemSimulation is the interactive CLI driver — reads commands from stdin and dispatches to the facade.
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class LibraryManagementInterface {
private final LibraryManagementSystem libraryManagementSystem;
public LibraryManagementInterface(String libraryId, Integer totalRacks) {
this.libraryManagementSystem = new LibraryManagementSystem(libraryId, totalRacks);
System.out.println("[INFO] | Created library with " + totalRacks + " racks");
}
public void addBook(String bookId, String title, String authors,
String publishingCompanies, String bookCopyIds) {
List<String> authorList = Arrays.asList(authors.split(","));
List<String> publishingCompanyList = Arrays.asList(publishingCompanies.split(","));
List<String> bookCopyIdList = Arrays.asList(bookCopyIds.split(","));
libraryManagementSystem.addBook(bookId, title, authorList, publishingCompanyList, bookCopyIdList);
}
public void removeBookCopy(String copyId) { libraryManagementSystem.removeBookCopy(copyId); }
public void addUser(String userId, String name) { libraryManagementSystem.addUser(userId, name); }
public void borrowBook(String bookId, String userId, Date date) { libraryManagementSystem.borrowBook(bookId, userId, date); }
public void borrowBookCopy(String copyId, String userId, Date date){ libraryManagementSystem.borrowBookCopy(copyId, userId, date); }
public void returnBookCopy(String bookCopyId) { libraryManagementSystem.returnBookCopy(bookCopyId); }
public void printBorrowedBooks(String userId) { libraryManagementSystem.printBorrowedBooks(userId); }
public void searchBooks(String attribute, String value) { libraryManagementSystem.searchBooks(attribute, value); }
public void displayRacks() { libraryManagementSystem.displayRacks(); }
public void getBorrowLimitPerUser() {
System.out.println("[DETAILS] | Borrow Limit: " + libraryManagementSystem.getBorrowLimitPerUser());
}
}import java.text.SimpleDateFormat;
import java.util.Scanner;
public class LibraryManagementSystemSimulation {
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
LibraryManagementInterface library = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
while (true) {
String input = scanner.nextLine().trim();
if (input.equalsIgnoreCase("exit")) break;
if (input.isEmpty()) continue;
String[] parts = input.split(" ", 2);
String command = parts[0];
String[] params = (parts.length > 1) ? parts[1].split(" ") : new String[0];
try {
switch (command) {
case "create_library" -> library = new LibraryManagementInterface(params[0], Integer.parseInt(params[1]));
case "add_book" -> { if (library != null) library.addBook(params[0], params[1], params[2], params[3], params[4]); }
case "remove_book_copy" -> { if (library != null) library.removeBookCopy(params[0]); }
case "add_user" -> { if (library != null) library.addUser(params[0], params[1]); }
case "borrow_book" -> { if (library != null) library.borrowBook(params[0], params[1], sdf.parse(params[2])); }
case "borrow_book_copy" -> { if (library != null) library.borrowBookCopy(params[0], params[1], sdf.parse(params[2])); }
case "return_book_copy" -> { if (library != null) library.returnBookCopy(params[0]); }
case "print_borrowed" -> { if (library != null) library.printBorrowedBooks(params[0]); }
case "search" -> { if (library != null) library.searchBooks(params[0], params[1]); }
case "display_racks" -> { if (library != null) library.displayRacks(); }
default -> System.out.println("[ERROR] | Unknown command: " + command);
}
} catch (Exception e) {
System.out.println("[ERROR] | Invalid parameters for: " + command);
}
}
System.out.println("Exiting...");
scanner.close();
}
}Class Diagram#
Extendible — Follow Ups#
1. Late fee calculation#
Add a FinePolicy interface with a calculateFine(Date dueDate, Date returnDate) → BigDecimal method. LibraryManagementSystem.returnBookCopy() calls the policy after marking the log. A StandardFinePolicy charges a fixed daily rate; a GracePeriodFinePolicy waives fees for the first 3 days. Swap policies at construction time without touching the return logic.
2. Reservation queue#
When no copy is available, a user should be able to join a waiting list. Add a Map<String, Queue<LibraryUser>> bookIdToWaitlist in LibraryManagementSystem. When returnBookCopy is called, check the queue: if non-empty, automatically borrow the copy to the head of the queue rather than placing it back on a rack. This is the Observer pattern applied to book availability.
3. Thread-safe concurrent borrow#
Two users borrowing the last copy simultaneously can cause a race. Wrap the borrow methods with synchronized(this) or use ReentrantLock per book ID. A finer-grained approach locks on the BookCopy object itself — only the copy being borrowed is locked, not the whole engine.
4. Search via Strategy objects#
Replace the switch inside searchBooks with a Map<String, SearchStrategy> where SearchStrategy is a @FunctionalInterface taking (Collection<BookCopy>, String) → List<BookCopy>. Register each attribute strategy at construction time. Adding a new searchable attribute becomes a one-line map.put(...) call — no switch to modify.
5. Persistence with Repository pattern#
Extract a BookCopyRepository, UserRepository, and BorrowLogRepository interface from LibraryManagementSystem. The current in-memory HashMap becomes the default implementation; a JdbcBookCopyRepository can swap in for production. The engine depends only on the interfaces, so the storage layer is fully swappable.
6. Book copy inherits vs. holds a Book#
The current BookCopy extends Book creates an IS-A relationship where composition (HAS-A) would be more accurate — a copy is not a book, it holds a reference to one. A follow-up refactor removes the extends Book, drops the super(...) constructor call, and exposes all book properties through getBook().getXxx() delegation. This is a SOLID Liskov Substitution improvement: no copy can be passed where a Book is expected.