Spend 3–5 minutes clarifying requirements before designing. A library system has more moving parts than it first appears — the reservation queue and fine strategy in particular are easy to forget.
- Maintain a catalog of books — each with metadata (title, author, ISBN, genre) and multiple physical copies (BookItems)
- Register members with different membership types: Student, Faculty, Public — each with different borrow limits and loan periods
- Allow members to borrow a book by ISBN — automatically assign the first available copy
- Track return dates and calculate overdue fines per day; support pluggable fine strategies
- Allow members to reserve a book when all copies are borrowed — maintained as a FIFO queue per title
- On return of a reserved copy, notify the next member in the reservation queue
- Support multi-channel notifications: email, SMS (pluggable Observer pattern)
- Search the catalog by title, author, or genre
- Prevent borrowing if member is suspended, membership is expired, or borrow limit is reached
- Thread-safe operations for concurrent borrow/return requests
- Singleton LibrarySystem per process — single source of truth for catalog and member state
- Admin: mark books as lost, under maintenance, or remove expired reservations
The most important design insight here is the Book / BookItem split. Beginners model a single "Book" class — but a library holds multiple copies of the same title. Separating logical metadata (Book) from the physical object (BookItem) is what makes the model correct.
| Class / Interface | Type | Responsibility |
|---|---|---|
| BookStatus | Enum | AVAILABLE · BORROWED · RESERVED · LOST · UNDER_MAINTENANCE |
| MembershipType | Enum | STUDENT · FACULTY · PUBLIC (controls borrow limits & loan days) |
| Book | Class | Logical title — ISBN, author, genre metadata |
| BookItem | Class | Physical copy with barcode, shelf location, status |
| Member | Class | Registered user with active borrows & reservations |
| BorrowRecord | Class | Borrow event — dates, due date, fine on return |
| Reservation | Class | Hold in FIFO queue per ISBN; expires if not collected |
| Catalog | Class | Dual-index store: ISBN→copies, barcode→BookItem |
| FineStrategy | Interface | calculate(record) → fine amount (Standard / Grace / Capped) |
| NotificationObserver | Interface | onNotify(memberId, type, msg) — Email / SMS / Push |
| NotificationService | Class | Subject holding list of observers; fires on events |
| LibrarySystem | Singleton | Facade orchestrating catalog, members, borrowing, notifications |
LibrarySystem (Singleton + Facade) ├── catalog: Catalog │ ├── books: Map<isbn, Book> ← logical metadata │ ├── copies: Map<isbn, List<BookItem>> ← physical copies │ └── barcodeIndex: Map<barcode, BookItem> ├── members: Map<memberId, Member> │ ├── activeBorrows: Map<barcode, BorrowRecord> │ └── activeReservations: List<Reservation> ├── reservationQueues: Map<isbn, Queue<Reservation>> ← FIFO per title ├── borrowHistory: List<BorrowRecord> ← audit log ├── notificationService: NotificationService │ └── observers: [EmailNotifier, SmsNotifier, ...] └── fineStrategy: FineStrategy ← Standard / GracePeriod / Capped
The borrow and return flows are the critical paths. The reservation queue interaction on return — notifying the next waiting member — is the most commonly missed detail in interviews.
Borrow Happy Path
canBorrow() check → first available copy found → BorrowRecord created → copy status BORROWED.
Reservation Trigger
When all copies are BORROWED, member is added to the FIFO queue. Queue position is shown for transparency.
Return + Notification
On return, fine calculated, then queue peeked. If next reservation is valid, copy → RESERVED and member notified immediately.
Expired Reservation
If the head of the queue has expired (member didn't collect in time), it's discarded and the next valid entry is checked.
Key relationships to call out on a whiteboard: Catalog composes BookItems (not Books directly — Books are just metadata), Member owns BorrowRecords (not the LibrarySystem — each member is responsible for their own borrows), and LibrarySystem delegates to FineStrategy and NotificationService (Strategy + Observer).
BookStatus drives the availability check — only AVAILABLE copies can be borrowed. MembershipType is the key discriminator used by both borrow-limit enforcement and loan-period calculation, so it appears as a parameter in multiple places.
// ── Enums ─────────────────────────────────────────────────────
public enum BookStatus { AVAILABLE, BORROWED, RESERVED, LOST, UNDER_MAINTENANCE }
public enum MemberStatus { ACTIVE, SUSPENDED, EXPIRED }
public enum MembershipType { STUDENT, FACULTY, PUBLIC }
public enum NotificationType { DUE_SOON, OVERDUE, RESERVATION_READY, MEMBERSHIP_EXPIRING }The Book/BookItem Split — The Core Design Insight
Book is the logical title — ISBN, author, metadata. A BookItem is a physical copy with a unique barcode and shelf location. The system can hold 5 copies of "1984" — each is a separate BookItem, but all share one Book record. Without this split, you cannot track individual copy status or location independently.The shelf location format "F3-S2-B7" (Floor-Shelf-Bay) is a real-world touch that interviewers appreciate — it shows awareness of operational usability beyond the data model.
// ── Book & BookItem ────────────────────────────────────────────
// Book = the logical title (metadata); BookItem = a physical copy.
// One Book can have many BookItems (copies).
public class Book {
private final String isbn;
private final String title;
private final String author;
private final String publisher;
private final int publicationYear;
private final String genre;
public Book(String isbn, String title, String author,
String publisher, int year, String genre) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.publisher = publisher;
this.publicationYear = year;
this.genre = genre;
}
public String getIsbn() { return isbn; }
public String getTitle() { return title; }
public String getAuthor() { return author; }
public String getPublisher() { return publisher; }
public int getPublicationYear() { return publicationYear; }
public String getGenre() { return genre; }
}
public class BookItem {
private final String barcode; // unique physical copy ID
private final Book book; // parent title metadata
private final String shelfLocation; // e.g. "F3-S2-B7"
private BookStatus status;
public BookItem(String barcode, Book book, String shelfLocation) {
this.barcode = barcode;
this.book = book;
this.shelfLocation = shelfLocation;
this.status = BookStatus.AVAILABLE;
}
public boolean isAvailable() { return status == BookStatus.AVAILABLE; }
public void updateStatus(BookStatus newStatus) { this.status = newStatus; }
public String getBarcode() { return barcode; }
public Book getBook() { return book; }
public String getShelfLocation() { return shelfLocation; }
public BookStatus getStatus() { return status; }
}Membership-Type-Driven Limits
MembershipType — not hardcoded in the Member class. Adding a new type (e.g., CORPORATE) only requires updating the limit map — no changes to borrowing logic. This is the Open/Closed Principle applied to data-driven config.canBorrow() checks three conditions atomically: active status, non-expired membership, and remaining borrow capacity. The method mutators (addBorrow, removeBorrow) have reduced visibility — only the LibrarySystem should call them.
// ── Member ─────────────────────────────────────────────────────
import java.time.LocalDate;
import java.util.*;
public class Member {
private final String memberId;
private final String name;
private final String email;
private final MembershipType membershipType;
private MemberStatus status;
private LocalDate membershipExpiry;
// Active borrows (barcode → BorrowRecord) and reservations
private final Map<String, BorrowRecord> activeBorrows;
private final List<Reservation> activeReservations;
// Limits per membership type
private static final Map<MembershipType, Integer> BORROW_LIMITS = Map.of(
MembershipType.STUDENT, 5,
MembershipType.FACULTY, 10,
MembershipType.PUBLIC, 3
);
public Member(String memberId, String name, String email,
MembershipType type, LocalDate expiry) {
this.memberId = memberId;
this.name = name;
this.email = email;
this.membershipType = type;
this.membershipExpiry = expiry;
this.status = MemberStatus.ACTIVE;
this.activeBorrows = new HashMap<>();
this.activeReservations = new ArrayList<>();
}
public boolean canBorrow() {
return status == MemberStatus.ACTIVE
&& !isMembershipExpired()
&& activeBorrows.size() < BORROW_LIMITS.get(membershipType);
}
public boolean isMembershipExpired() {
return LocalDate.now().isAfter(membershipExpiry);
}
public int getBorrowLimit() { return BORROW_LIMITS.get(membershipType); }
public int getCurrentBorrows() { return activeBorrows.size(); }
// Getters
public String getMemberId() { return memberId; }
public String getName() { return name; }
public String getEmail() { return email; }
public MembershipType getMembershipType(){ return membershipType; }
public MemberStatus getStatus() { return status; }
public LocalDate getMembershipExpiry(){ return membershipExpiry; }
public Map<String, BorrowRecord> getActiveBorrows() { return Collections.unmodifiableMap(activeBorrows); }
public List<Reservation> getActiveReservations() { return Collections.unmodifiableList(activeReservations); }
// Package-level mutators — called only by LibrarySystem
void addBorrow(BorrowRecord record) { activeBorrows.put(record.getBarcode(), record); }
void removeBorrow(String barcode) { activeBorrows.remove(barcode); }
void addReservation(Reservation res) { activeReservations.add(res); }
void removeReservation(Reservation res) { activeReservations.remove(res); }
void suspend() { this.status = MemberStatus.SUSPENDED; }
void reinstate() { this.status = MemberStatus.ACTIVE; }
}BorrowRecord is the contract between the library and the member. It captures the loan period based on membership type at the time of borrowing — Faculty get 30 days, Students 14, Public 7. The fine is calculated lazily on markReturned().
dueDateborrowDate + LOAN_DAYS[membershipType]
calculateFine(today - dueDate) × FINE_PER_DAY, only if overdue
expiresOnReservation auto-expires after 3 days if not collected
FIFO queueLinkedList/deque per ISBN — first reserved, first notified
// ── BorrowRecord & Reservation ─────────────────────────────────
import java.time.LocalDate;
import java.util.UUID;
public class BorrowRecord {
private final String recordId;
private final String memberId;
private final String barcode; // BookItem barcode
private final LocalDate borrowDate;
private final LocalDate dueDate;
private LocalDate returnDate;
private double fineAmount;
// Default loan periods (days) per membership type
public static final Map<MembershipType, Integer> LOAN_DAYS = Map.of(
MembershipType.STUDENT, 14,
MembershipType.FACULTY, 30,
MembershipType.PUBLIC, 7
);
public static final double FINE_PER_DAY = 2.0; // ₹2 per overdue day
public BorrowRecord(String memberId, String barcode, MembershipType type) {
this.recordId = UUID.randomUUID().toString();
this.memberId = memberId;
this.barcode = barcode;
this.borrowDate = LocalDate.now();
this.dueDate = borrowDate.plusDays(LOAN_DAYS.get(type));
this.fineAmount = 0.0;
}
public boolean isOverdue() { return LocalDate.now().isAfter(dueDate) && returnDate == null; }
public double calculateFine() {
if (!isOverdue()) return 0.0;
long overdueDays = java.time.temporal.ChronoUnit.DAYS.between(dueDate, LocalDate.now());
return overdueDays * FINE_PER_DAY;
}
public void markReturned() {
this.returnDate = LocalDate.now();
this.fineAmount = calculateFine();
}
public String getRecordId() { return recordId; }
public String getMemberId() { return memberId; }
public String getBarcode() { return barcode; }
public LocalDate getBorrowDate() { return borrowDate; }
public LocalDate getDueDate() { return dueDate; }
public LocalDate getReturnDate() { return returnDate; }
public double getFineAmount() { return fineAmount; }
}
// ────────────────────────────────────────────────────────────────
public class Reservation {
private final String reservationId;
private final String memberId;
private final String isbn; // logical book (any copy)
private final LocalDate reservedOn;
private LocalDate expiresOn; // auto-cancel if not collected
private boolean fulfilled;
public static final int RESERVATION_HOLD_DAYS = 3; // hold for 3 days after available
public Reservation(String memberId, String isbn) {
this.reservationId = UUID.randomUUID().toString();
this.memberId = memberId;
this.isbn = isbn;
this.reservedOn = LocalDate.now();
this.expiresOn = reservedOn.plusDays(RESERVATION_HOLD_DAYS);
this.fulfilled = false;
}
public boolean isExpired() { return LocalDate.now().isAfter(expiresOn) && !fulfilled; }
public void fulfill() { this.fulfilled = true; }
public void setExpiry(LocalDate d) { this.expiresOn = d; }
public String getReservationId() { return reservationId; }
public String getMemberId() { return memberId; }
public String getIsbn() { return isbn; }
public LocalDate getReservedOn() { return reservedOn; }
public LocalDate getExpiresOn() { return expiresOn; }
public boolean isFulfilled() { return fulfilled; }
}Dual-Index Design
isbn→Book (title metadata),isbn→List<BookItem> (all copies of a title), andbarcode→BookItem (fast single-copy lookup on return). All three are O(1). The title/author/genre search is O(N) — a conscious tradeoff worth mentioning.// ── Catalog ─────────────────────────────────────────────────────
import java.util.*;
import java.util.stream.Collectors;
public class Catalog {
// isbn → Book metadata
private final Map<String, Book> books;
// isbn → List of physical copies (BookItems)
private final Map<String, List<BookItem>> copies;
// barcode → BookItem (fast lookup by physical copy)
private final Map<String, BookItem> barcodeIndex;
public Catalog() {
this.books = new HashMap<>();
this.copies = new HashMap<>();
this.barcodeIndex = new HashMap<>();
}
public void addBook(Book book) {
books.put(book.getIsbn(), book);
copies.putIfAbsent(book.getIsbn(), new ArrayList<>());
}
public void addCopy(BookItem item) {
String isbn = item.getBook().getIsbn();
if (!books.containsKey(isbn))
throw new IllegalArgumentException("Book not in catalog: " + isbn);
copies.get(isbn).add(item);
barcodeIndex.put(item.getBarcode(), item);
}
public Optional<Book> findByIsbn(String isbn) { return Optional.ofNullable(books.get(isbn)); }
public Optional<BookItem> findByBarcode(String barcode) { return Optional.ofNullable(barcodeIndex.get(barcode)); }
public List<BookItem> getAvailableCopies(String isbn) {
return copies.getOrDefault(isbn, List.of()).stream()
.filter(BookItem::isAvailable)
.collect(Collectors.toList());
}
/** Search by title substring (case-insensitive) */
public List<Book> searchByTitle(String query) {
String q = query.toLowerCase();
return books.values().stream()
.filter(b -> b.getTitle().toLowerCase().contains(q))
.collect(Collectors.toList());
}
/** Search by author substring */
public List<Book> searchByAuthor(String query) {
String q = query.toLowerCase();
return books.values().stream()
.filter(b -> b.getAuthor().toLowerCase().contains(q))
.collect(Collectors.toList());
}
/** Search by genre */
public List<Book> searchByGenre(String genre) {
return books.values().stream()
.filter(b -> b.getGenre().equalsIgnoreCase(genre))
.collect(Collectors.toList());
}
public int totalCopies(String isbn) { return copies.getOrDefault(isbn, List.of()).size(); }
public int availableCopies(String isbn) { return getAvailableCopies(isbn).size(); }
public Map<String, Book> getAllBooks() { return Collections.unmodifiableMap(books); }
}Observer Pattern
NotificationService is the Subject.EmailNotifier, SmsNotifier are Observers. LibrarySystem fires events without knowing how delivery works. Adding a WhatsApp notifier requires implementing NotificationObserver and callingaddNotifier() at startup — zero changes to core logic.// ── Notification Service (Observer Pattern) ───────────────────
public interface NotificationObserver {
void onNotify(String memberId, NotificationType type, String message);
}
// Concrete: Email
public class EmailNotifier implements NotificationObserver {
@Override
public void onNotify(String memberId, NotificationType type, String message) {
System.out.printf("[EMAIL] Member %s | %s: %s%n", memberId, type, message);
// In production: call SMTP / SendGrid API here
}
}
// Concrete: SMS
public class SmsNotifier implements NotificationObserver {
@Override
public void onNotify(String memberId, NotificationType type, String message) {
System.out.printf("[SMS] Member %s | %s: %s%n", memberId, type, message);
// In production: call Twilio / SNS API here
}
}
// Subject that holds observers
public class NotificationService {
private final List<NotificationObserver> observers = new ArrayList<>();
public void subscribe(NotificationObserver obs) { observers.add(obs); }
public void unsubscribe(NotificationObserver obs) { observers.remove(obs); }
public void notify(String memberId, NotificationType type, String message) {
for (NotificationObserver obs : observers)
obs.onNotify(memberId, type, message);
}
}Strategy Pattern for Fine Calculation
FineStrategy decouples fine calculation from the return flow. The library can swap from StandardFine (₹2/day from day 1) to GracePeriodFine (first 1 day free) during exams, or CappedFine for public members — without touching LibrarySystem.// ── Fine Calculator (Strategy Pattern) ────────────────────────
public interface FineStrategy {
double calculate(BorrowRecord record);
}
public class StandardFine implements FineStrategy {
@Override
public double calculate(BorrowRecord record) {
return record.calculateFine(); // ₹2/day as defined in BorrowRecord
}
}
public class GracePeriodFine implements FineStrategy {
private final int graceDays;
public GracePeriodFine(int graceDays) { this.graceDays = graceDays; }
@Override
public double calculate(BorrowRecord record) {
if (!record.isOverdue()) return 0.0;
long overdueDays = java.time.temporal.ChronoUnit.DAYS.between(
record.getDueDate(), java.time.LocalDate.now());
long chargeableDays = Math.max(0, overdueDays - graceDays);
return chargeableDays * BorrowRecord.FINE_PER_DAY;
}
}
public class CappedFine implements FineStrategy {
private final double maxFine;
public CappedFine(double maxFine) { this.maxFine = maxFine; }
@Override
public double calculate(BorrowRecord record) {
return Math.min(record.calculateFine(), maxFine);
}
}Thread Safety
borrowBook(), returnBook(), andreserveBook() all acquire a ReentrantLock. Without locking, two members could simultaneously borrow the last copy of a title — both pass the availability check and both get the book — causing negative stock and data corruption.LibrarySystem is also a Facade — it provides a simple, unified interface over the complex subsystem (Catalog, Member, BorrowRecord, Reservation, NotificationService, FineStrategy). Callers never directly manipulate BookItem status or Member borrows.
// ── LibrarySystem (Singleton + Facade) ─────────────────────────
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
public class LibrarySystem {
private static volatile LibrarySystem instance;
private final ReentrantLock lock = new ReentrantLock();
private final String libraryName;
private final Catalog catalog;
private final Map<String, Member> members; // memberId → Member
// isbn → Queue of reservations (FIFO per book)
private final Map<String, Queue<Reservation>> reservationQueues;
private final List<BorrowRecord> borrowHistory;
private final NotificationService notificationService;
private FineStrategy fineStrategy;
private LibrarySystem(String name) {
this.libraryName = name;
this.catalog = new Catalog();
this.members = new HashMap<>();
this.reservationQueues = new HashMap<>();
this.borrowHistory = new ArrayList<>();
this.notificationService = new NotificationService();
this.fineStrategy = new StandardFine();
}
// ── Singleton ──────────────────────────────────────────────
public static LibrarySystem getInstance(String name) {
if (instance == null) {
synchronized (LibrarySystem.class) {
if (instance == null) instance = new LibrarySystem(name);
}
}
return instance;
}
// ── Setup ──────────────────────────────────────────────────
public void addNotifier(NotificationObserver obs) {
notificationService.subscribe(obs);
}
public void setFineStrategy(FineStrategy strategy) {
this.fineStrategy = strategy;
}
// ── Member Management ──────────────────────────────────────
public void registerMember(Member member) {
lock.lock();
try { members.put(member.getMemberId(), member); }
finally { lock.unlock(); }
}
public Optional<Member> findMember(String memberId) {
return Optional.ofNullable(members.get(memberId));
}
// ── Borrow ─────────────────────────────────────────────────
/**
* Borrow a book by ISBN (automatically picks the first available copy).
* Throws if no copies available or member cannot borrow.
*/
public BorrowRecord borrowBook(String memberId, String isbn) {
lock.lock();
try {
Member member = getMemberOrThrow(memberId);
if (!member.canBorrow())
throw new IllegalStateException(
"Member cannot borrow: check status, expiry, or borrow limit.");
List<BookItem> available = catalog.getAvailableCopies(isbn);
if (available.isEmpty())
throw new IllegalStateException("No copies available for ISBN: " + isbn);
BookItem item = available.get(0);
item.updateStatus(BookStatus.BORROWED);
BorrowRecord record = new BorrowRecord(memberId, item.getBarcode(),
member.getMembershipType());
member.addBorrow(record);
borrowHistory.add(record);
System.out.printf("Borrowed '%s' (copy: %s) by %s. Due: %s%n",
item.getBook().getTitle(), item.getBarcode(), member.getName(),
record.getDueDate());
return record;
} finally { lock.unlock(); }
}
// ── Return ─────────────────────────────────────────────────
/**
* Return a book copy by barcode. Calculates fine if overdue,
* then checks if a reservation queue exists for this title
* and notifies the next waiting member.
*/
public double returnBook(String memberId, String barcode) {
lock.lock();
try {
Member member = getMemberOrThrow(memberId);
BookItem item = catalog.findByBarcode(barcode)
.orElseThrow(() -> new IllegalArgumentException("Unknown barcode: " + barcode));
BorrowRecord record = Optional.ofNullable(
member.getActiveBorrows().get(barcode))
.orElseThrow(() -> new IllegalArgumentException(
"No active borrow for this copy by member " + memberId));
record.markReturned();
double fine = fineStrategy.calculate(record);
member.removeBorrow(barcode);
if (fine > 0) {
System.out.printf("Fine for %s: ₹%.2f%n", member.getName(), fine);
notificationService.notify(memberId, NotificationType.OVERDUE,
String.format("Overdue fine: ₹%.2f for '%s'",
fine, item.getBook().getTitle()));
}
// Check reservation queue for this title
String isbn = item.getBook().getIsbn();
Queue<Reservation> queue = reservationQueues.get(isbn);
if (queue != null && !queue.isEmpty()) {
Reservation next = queue.peek();
if (!next.isExpired()) {
item.updateStatus(BookStatus.RESERVED);
notificationService.notify(next.getMemberId(),
NotificationType.RESERVATION_READY,
String.format("'%s' is ready for pickup. Collect by %s.",
item.getBook().getTitle(), next.getExpiresOn()));
} else {
queue.poll(); // expired — discard and make available
item.updateStatus(BookStatus.AVAILABLE);
}
} else {
item.updateStatus(BookStatus.AVAILABLE);
}
return fine;
} finally { lock.unlock(); }
}
// ── Reserve ────────────────────────────────────────────────
/**
* Reserve a book by ISBN when all copies are borrowed.
* Adds to FIFO queue — first-reserved, first-served.
*/
public Reservation reserveBook(String memberId, String isbn) {
lock.lock();
try {
Member member = getMemberOrThrow(memberId);
if (member.getStatus() != MemberStatus.ACTIVE)
throw new IllegalStateException("Member account is not active.");
// Cannot reserve if a copy is already available
if (!catalog.getAvailableCopies(isbn).isEmpty())
throw new IllegalStateException(
"Copies are available — borrow directly instead of reserving.");
Reservation res = new Reservation(memberId, isbn);
reservationQueues.computeIfAbsent(isbn, k -> new LinkedList<>()).add(res);
member.addReservation(res);
System.out.printf("Reservation created for %s (ISBN: %s). Queue position: %d%n",
member.getName(), isbn,
new ArrayList<>(reservationQueues.get(isbn)).indexOf(res) + 1);
return res;
} finally { lock.unlock(); }
}
// ── Search ─────────────────────────────────────────────────
public List<Book> searchByTitle(String query) { return catalog.searchByTitle(query); }
public List<Book> searchByAuthor(String query) { return catalog.searchByAuthor(query); }
public List<Book> searchByGenre(String genre) { return catalog.searchByGenre(genre); }
// ── Overdue check (scheduled job) ──────────────────────────
public void runOverdueCheck() {
for (Member member : members.values()) {
for (BorrowRecord record : member.getActiveBorrows().values()) {
if (record.isOverdue()) {
notificationService.notify(member.getMemberId(),
NotificationType.OVERDUE,
String.format("'%s' is overdue since %s. Fine: ₹%.2f/day.",
catalog.findByBarcode(record.getBarcode())
.map(i -> i.getBook().getTitle()).orElse("Unknown"),
record.getDueDate(), BorrowRecord.FINE_PER_DAY));
}
}
}
}
public Catalog getCatalog() { return catalog; }
private Member getMemberOrThrow(String memberId) {
return Optional.ofNullable(members.get(memberId))
.orElseThrow(() -> new IllegalArgumentException("Member not found: " + memberId));
}
}The demo covers: borrowing (both copies of a title), reserving when none are available, returning with automatic reservation queue notification, searching the catalog, and triggering an overdue check.
// ── Demo ───────────────────────────────────────────────────────
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
LibrarySystem lib = LibrarySystem.getInstance("City Public Library");
// Attach notification channels
lib.addNotifier(new EmailNotifier());
lib.setFineStrategy(new GracePeriodFine(1)); // 1-day grace period
// Add books & copies to catalog
Catalog cat = lib.getCatalog();
Book b1 = new Book("978-0-06-112008-4", "To Kill a Mockingbird",
"Harper Lee", "HarperCollins", 1960, "Fiction");
Book b2 = new Book("978-0-7432-7356-5", "1984",
"George Orwell", "Secker & Warburg", 1949, "Dystopia");
cat.addBook(b1);
cat.addBook(b2);
cat.addCopy(new BookItem("BC001", b1, "F3-S1-B1"));
cat.addCopy(new BookItem("BC002", b1, "F3-S1-B2")); // second copy
cat.addCopy(new BookItem("BC003", b2, "F3-S2-B1"));
// Register members
Member alice = new Member("M001", "Alice", "alice@example.com",
MembershipType.STUDENT, LocalDate.now().plusYears(1));
Member bob = new Member("M002", "Bob", "bob@example.com",
MembershipType.FACULTY, LocalDate.now().plusYears(1));
lib.registerMember(alice);
lib.registerMember(bob);
// ── Borrow ─────────────────────────────────────────────
System.out.println("=== Borrow ===");
BorrowRecord r1 = lib.borrowBook("M001", "978-0-06-112008-4"); // Alice borrows copy 1
BorrowRecord r2 = lib.borrowBook("M002", "978-0-06-112008-4"); // Bob borrows copy 2
// ── Reserve when no copies left ────────────────────────
System.out.println("
=== Reserve ===");
// A third member tries — all copies out
Member carol = new Member("M003", "Carol", "carol@example.com",
MembershipType.PUBLIC, LocalDate.now().plusMonths(6));
lib.registerMember(carol);
Reservation res = lib.reserveBook("M003", "978-0-06-112008-4");
System.out.println("Carol's reservation ID: " + res.getReservationId());
// ── Return (no fine — not overdue) ────────────────────
System.out.println("
=== Return ===");
double fine = lib.returnBook("M001", "BC001"); // Alice returns BC001
// Carol should be notified now (RESERVATION_READY)
// ── Search ────────────────────────────────────────────
System.out.println("
=== Search ===");
lib.searchByAuthor("Orwell").forEach(b -> System.out.println("Found: " + b.getTitle()));
lib.searchByGenre("Fiction").forEach(b -> System.out.println("Genre hit: " + b.getTitle()));
// ── Overdue simulation ────────────────────────────────
System.out.println("
=== Overdue Check ===");
lib.runOverdueCheck(); // In prod, scheduled via cron / Spring @Scheduled
}
}Expected output:
=== Borrow ===
Borrowed 'To Kill a Mockingbird' (copy: BC001) by Alice. Due: 2025-05-02
Borrowed 'To Kill a Mockingbird' (copy: BC002) by Bob. Due: 2025-05-16
=== Reserve ===
Reservation for Carol (ISBN: 978-0-06-112008-4). Queue position: 1
Carol's reservation ID: a4f2e1...
=== Return ===
[EMAIL] Member M003 | RESERVATION_READY: 'To Kill a Mockingbird' is ready. Collect by 2025-04-21.
=== Search ===
Found: 1984
Genre hit: To Kill a Mockingbird
=== Overdue Check ===
[EMAIL] Member M002 | OVERDUE: '...' overdue since ... Fine: ₹2.0/day.
Library systems have subtle concurrency bugs. Here are the three most important to discuss:
Race Condition: Double-Borrow of Last Copy
Without locking: Thread A calls getAvailableCopies() — sees [BC001]. Thread B simultaneously calls getAvailableCopies() — also sees [BC001]. Both proceed to borrow BC001, both call item.updateStatus(BORROWED). Two members claim the same physical copy.
✓ Fix: The entire findCopy + updateStatus + createRecord sequence is wrapped in a single lock, making it atomic.
Race Condition: Reservation Queue + Return
Without locking: Thread A returns a book and checks the reservation queue — queue is non-empty. Before Thread A calls item.updateStatus(RESERVED), Thread B also borrows (a different copy returned simultaneously) and sets the copy to BORROWED. Now the reserved copy is BORROWED and the waiting member never gets notified.
✓ Fix: returnBook() acquires the lock before checking queue and updating status — the check-then-set is atomic.
Singleton Race on Startup
Two threads simultaneously check instance == null, both see null, both construct a LibrarySystem. Each gets its own empty catalog and member map — two independent instances, neither aware of the other's state.
✓ Fix: volatile + synchronized (Java) / threading.Lock (Python) double-checked locking ensures single construction.
| Operation | Time | Notes |
|---|---|---|
| borrowBook(memberId, isbn) | O(C) | C = copies per title; scan for first available. Usually tiny. |
| returnBook(memberId, barcode) | O(1) | HashMap lookup on barcode + queue peek. |
| reserveBook(memberId, isbn) | O(1) | Deque append + member list append. |
| findByBarcode(barcode) | O(1) | Direct HashMap lookup. |
| searchByTitle(query) | O(B) | B = books in catalog; full substring scan. Use inverted index at scale. |
| runOverdueCheck() | O(M×B) | M = members, B = avg borrows per member. Run as batch job. |
| calculateFine(record) | O(1) | Date arithmetic only. |
WHERE due_date < NOW() AND return_date IS NULL query on a database index — far faster than in-memory iteration across millions of records.Mention 2–3 of these to demonstrate forward-thinking design. Don't implement — just explain the pattern.
E-Book / Digital Items
Subclass BookItem → EBookItem with a downloadUrl and DRM token. borrowBook() for e-books skips shelf logic and issues a timed download link instead.
Self-Service Member Portal
Member class exposes a renew() method — extends dueDate by one loan period if no pending reservation on that copy. Prevents unnecessary returns when the item isn't needed by others.
Full-Text Search
Push Book metadata to Elasticsearch on catalog.addBook(). searchByTitle/Author become async calls to the ES index — O(log N) instead of O(N) and supports fuzzy/typo-tolerant search.
Fine Payment Integration
Add a FineAccount per member (balance, paid history). returnBook() posts a FineTransaction to the account. Members pay via a PaymentGateway (Strategy pattern, same as vending machine).
Automated Overdue Scheduler
Move runOverdueCheck() to a @Scheduled (Spring) / APScheduler (Python) job running nightly. Emit OVERDUE events for members with 3+ overdue days; suspend account at 14+ days.
Role-Based Access Control
Add a Role enum (MEMBER, LIBRARIAN, ADMIN). Method-level guards: only LIBRARIANs can mark a copy LOST or UNDER_MAINTENANCE; only ADMINs can suspend/delete member accounts.
17. Key Design Decisions
Book vs. BookItem separation
Book holds logical metadata (ISBN, title, author). BookItem is a physical copy with a unique barcode and shelf location. This allows one title to have many copies and tracks each copy's status independently — a crucial real-world distinction.
FIFO Reservation Queue per ISBN
Reservations are held in a LinkedList/deque queue per ISBN. On return, the head of the queue is notified first. Expired reservations are purged lazily at return time — simpler than a background scheduler and avoids unnecessary complexity.
Observer Pattern for Notifications
NotificationService holds a list of NotificationObserver implementations (Email, SMS, Push). LibrarySystem fires events without knowing or caring how delivery works. Adding WhatsApp notifications requires zero changes to the core system.
Linear search for catalog queries
searchByTitle/Author/Genre performs a full O(N) scan. For large catalogs (100k+ books), this should be replaced with an inverted index or delegated to a full-text search engine like Elasticsearch or Meilisearch.
18. Common Follow-up Interview Questions
- How would you handle a member wanting to renew a borrowed book?
💡 Add renew() to Member — extends dueDate by one loan period, but only if no active reservation in the queue for that ISBN.
- How do you handle two members simultaneously trying to borrow the last copy?
💡 The whole check-then-borrow is inside a synchronized block / ReentrantLock. Only one thread proceeds; the other is rejected and should call reserveBook() instead.
- What if the reservation queue needs priority (e.g., Faculty before Students)?
💡 Replace LinkedList/deque with a PriorityQueue ordered by MembershipType priority, then by reservation timestamp as a tiebreaker.
- How would you add e-book support?
💡 Subclass BookItem → EBookItem. Override isAvailable() to always return true (unlimited digital copies) up to a concurrent-access limit. borrow() issues a timed token instead of updating a physical status.
- How do you scale search to 1 million books?
💡 Push Book metadata to Elasticsearch on insert. searchByTitle becomes an ES query — O(log N), fuzzy-tolerant. The in-memory scan is fine for thousands, not millions.
- How would you implement an overdue notification system that runs daily?
💡 Scheduled job (cron / @Scheduled / APScheduler) calls runOverdueCheck(). Better: query DB for WHERE due_date < today AND return_date IS NULL — far faster than in-memory iteration.
- How would you add fine payment processing?
💡 FineAccount per member; returnBook posts a FineTransaction. A PaymentGateway interface (Strategy) handles cash / card / UPI settlement — mirrors the vending machine payment approach.
- How do you prevent a suspended member from reserving or renewing?
💡 canBorrow() already checks status == ACTIVE. reserveBook() and renew() should add the same check — single source of truth on the Member.status enum.