Designing a Library Management System
A library management system is a classic low-level design problem that demonstrates how to model real-world entities, implement business logic, and apply object-oriented design principles effectively.
Problem Statement
Design a library management system that can handle books, members, and library operations efficiently.
Core Requirements
Book Management:
- Add, update, and remove books from the library catalog
- Track book details (title, author, ISBN, publication year, genre)
- Support multiple copies of the same book
- Handle different book formats (physical, digital, audiobook)
Member Management:
- Register new members and manage member profiles
- Support different membership types (student, faculty, general)
- Track member borrowing history and current loans
- Handle membership renewals and cancellations
Borrowing System:
- Allow members to check out and return books
- Implement due date tracking and overdue notifications
- Support book reservations and hold queues
- Calculate and manage late fees
Search & Discovery:
- Search books by title, author, ISBN, or genre
- Browse books by category
- Check book availability in real-time
- Recommend books based on borrowing history
Administrative Features:
- Generate reports on book popularity, member activity
- Manage library inventory and acquisitions
- Handle damaged or lost books
- Track library statistics
Requirements Analysis & Clarifying Questions
Before designing the system, let's clarify some ambiguities:
Functional Clarifications
- Book Copies: How many copies of the same book can the library have?
- Loan Duration: What's the default loan period? Does it vary by membership type?
- Reservations: Can members reserve books that are currently checked out?
- Renewals: Can books be renewed? How many times?
- Digital Books: Do digital books have borrowing limits like physical books?
Non-Functional Clarifications
- Scale: How many books and members should the system support?
- Concurrent Access: How many simultaneous users?
- Performance: What are the acceptable response times for searches?
- Integration: Does it need to integrate with external systems?
Business Rules
- Maximum books a member can borrow simultaneously
- Late fee calculation method
- Book reservation queue management
- Membership tier privileges
Core Entities & Relationships
Let's identify the main entities and their relationships:
Library (1) ←→ (many) Book
Library (1) ←→ (many) Member
Member (many) ←→ (many) BookItem [through Loan]
Book (1) ←→ (many) BookItem
Member (1) ←→ (many) Reservation
BookItem (1) ←→ (many) Reservation
Class Design
Book Hierarchy
// Base book entity
public abstract class Book {
private String ISBN;
private String title;
private String subject;
private String publisher;
private String language;
private int numberOfPages;
private List<Author> authors;
public Book(String ISBN, String title, String subject,
String publisher, String language, int numberOfPages) {
this.ISBN = ISBN;
this.title = title;
this.subject = subject;
this.publisher = publisher;
this.language = language;
this.numberOfPages = numberOfPages;
this.authors = new ArrayList<>();
}
// Getters and setters
public String getISBN() { return ISBN; }
public String getTitle() { return title; }
public String getSubject() { return subject; }
public List<Author> getAuthors() { return authors; }
public void addAuthor(Author author) {
authors.add(author);
}
}
// Concrete book implementations
public class PhysicalBook extends Book {
private String rackNumber;
private BookStatus status;
public PhysicalBook(String ISBN, String title, String subject,
String publisher, String language, int numberOfPages,
String rackNumber) {
super(ISBN, title, subject, publisher, language, numberOfPages);
this.rackNumber = rackNumber;
this.status = BookStatus.AVAILABLE;
}
public String getRackNumber() { return rackNumber; }
public BookStatus getStatus() { return status; }
public void setStatus(BookStatus status) { this.status = status; }
}
public class DigitalBook extends Book {
private String downloadURL;
private String format; // PDF, EPUB, etc.
private int maxConcurrentUsers;
private int currentActiveUsers;
public DigitalBook(String ISBN, String title, String subject,
String publisher, String language, int numberOfPages,
String downloadURL, String format, int maxConcurrentUsers) {
super(ISBN, title, subject, publisher, language, numberOfPages);
this.downloadURL = downloadURL;
this.format = format;
this.maxConcurrentUsers = maxConcurrentUsers;
this.currentActiveUsers = 0;
}
public boolean isAvailable() {
return currentActiveUsers < maxConcurrentUsers;
}
public boolean checkOut() {
if (isAvailable()) {
currentActiveUsers++;
return true;
}
return false;
}
public void returnBook() {
if (currentActiveUsers > 0) {
currentActiveUsers--;
}
}
}
public class AudioBook extends Book {
private String audioURL;
private int durationMinutes;
private String narrator;
public AudioBook(String ISBN, String title, String subject,
String publisher, String language, int numberOfPages,
String audioURL, int durationMinutes, String narrator) {
super(ISBN, title, subject, publisher, language, numberOfPages);
this.audioURL = audioURL;
this.durationMinutes = durationMinutes;
this.narrator = narrator;
}
// Getters
public String getAudioURL() { return audioURL; }
public int getDurationMinutes() { return durationMinutes; }
public String getNarrator() { return narrator; }
}
Member Hierarchy
// Base member class
public abstract class Member {
private String memberId;
private String name;
private String email;
private String phone;
private Address address;
private Date membershipDate;
private MembershipStatus status;
private List<Loan> currentLoans;
private List<Reservation> reservations;
public Member(String memberId, String name, String email, String phone) {
this.memberId = memberId;
this.name = name;
this.email = email;
this.phone = phone;
this.membershipDate = new Date();
this.status = MembershipStatus.ACTIVE;
this.currentLoans = new ArrayList<>();
this.reservations = new ArrayList<>();
}
// Abstract methods to be implemented by concrete classes
public abstract int getMaxBooksAllowed();
public abstract int getLoanDurationDays();
public abstract boolean canReserveBooks();
public abstract double getLateFeePerDay();
// Common methods
public boolean canBorrowBook() {
return currentLoans.size() < getMaxBooksAllowed() &&
status == MembershipStatus.ACTIVE;
}
public void addLoan(Loan loan) {
currentLoans.add(loan);
}
public void removeLoan(Loan loan) {
currentLoans.remove(loan);
}
// Getters and setters
public String getMemberId() { return memberId; }
public String getName() { return name; }
public String getEmail() { return email; }
public List<Loan> getCurrentLoans() { return currentLoans; }
public MembershipStatus getStatus() { return status; }
public void setStatus(MembershipStatus status) { this.status = status; }
}
// Concrete member types
public class StudentMember extends Member {
private String studentId;
private String institution;
public StudentMember(String memberId, String name, String email,
String phone, String studentId, String institution) {
super(memberId, name, email, phone);
this.studentId = studentId;
this.institution = institution;
}
@Override
public int getMaxBooksAllowed() { return 5; }
@Override
public int getLoanDurationDays() { return 30; }
@Override
public boolean canReserveBooks() { return true; }
@Override
public double getLateFeePerDay() { return 0.50; }
}
public class FacultyMember extends Member {
private String department;
private String employeeId;
public FacultyMember(String memberId, String name, String email,
String phone, String department, String employeeId) {
super(memberId, name, email, phone);
this.department = department;
this.employeeId = employeeId;
}
@Override
public int getMaxBooksAllowed() { return 15; }
@Override
public int getLoanDurationDays() { return 60; }
@Override
public boolean canReserveBooks() { return true; }
@Override
public double getLateFeePerDay() { return 1.00; }
}
public class GeneralMember extends Member {
private MembershipType membershipType;
public GeneralMember(String memberId, String name, String email,
String phone, MembershipType membershipType) {
super(memberId, name, email, phone);
this.membershipType = membershipType;
}
@Override
public int getMaxBooksAllowed() {
return membershipType == MembershipType.PREMIUM ? 10 : 3;
}
@Override
public int getLoanDurationDays() {
return membershipType == MembershipType.PREMIUM ? 21 : 14;
}
@Override
public boolean canReserveBooks() {
return membershipType == MembershipType.PREMIUM;
}
@Override
public double getLateFeePerDay() { return 1.00; }
}
Core Business Objects
// Loan transaction
public class Loan {
private String loanId;
private Member member;
private PhysicalBook book;
private Date issueDate;
private Date dueDate;
private Date returnDate;
private LoanStatus status;
private double lateFee;
public Loan(String loanId, Member member, PhysicalBook book) {
this.loanId = loanId;
this.member = member;
this.book = book;
this.issueDate = new Date();
this.dueDate = calculateDueDate();
this.status = LoanStatus.ACTIVE;
this.lateFee = 0.0;
}
private Date calculateDueDate() {
Calendar cal = Calendar.getInstance();
cal.setTime(issueDate);
cal.add(Calendar.DAY_OF_MONTH, member.getLoanDurationDays());
return cal.getTime();
}
public boolean isOverdue() {
return new Date().after(dueDate) && status == LoanStatus.ACTIVE;
}
public void returnBook() {
this.returnDate = new Date();
this.status = LoanStatus.RETURNED;
if (isOverdue()) {
calculateLateFee();
}
book.setStatus(BookStatus.AVAILABLE);
}
private void calculateLateFee() {
long overdueDays = (returnDate.getTime() - dueDate.getTime()) / (1000 * 60 * 60 * 24);
this.lateFee = overdueDays * member.getLateFeePerDay();
}
// Getters
public String getLoanId() { return loanId; }
public Member getMember() { return member; }
public PhysicalBook getBook() { return book; }
public Date getDueDate() { return dueDate; }
public LoanStatus getStatus() { return status; }
public double getLateFee() { return lateFee; }
}
// Book reservation
public class Reservation {
private String reservationId;
private Member member;
private Book book;
private Date reservationDate;
private Date expiryDate;
private ReservationStatus status;
public Reservation(String reservationId, Member member, Book book) {
this.reservationId = reservationId;
this.member = member;
this.book = book;
this.reservationDate = new Date();
this.expiryDate = calculateExpiryDate();
this.status = ReservationStatus.PENDING;
}
private Date calculateExpiryDate() {
Calendar cal = Calendar.getInstance();
cal.setTime(reservationDate);
cal.add(Calendar.DAY_OF_MONTH, 3); // 3 days to pick up
return cal.getTime();
}
public boolean isExpired() {
return new Date().after(expiryDate);
}
// Getters and setters
public String getReservationId() { return reservationId; }
public Member getMember() { return member; }
public Book getBook() { return book; }
public ReservationStatus getStatus() { return status; }
public void setStatus(ReservationStatus status) { this.status = status; }
}
// Supporting classes
public class Author {
private String name;
private String biography;
private Date birthDate;
public Author(String name, String biography, Date birthDate) {
this.name = name;
this.biography = biography;
this.birthDate = birthDate;
}
// Getters
public String getName() { return name; }
public String getBiography() { return biography; }
public Date getBirthDate() { return birthDate; }
}
public class Address {
private String street;
private String city;
private String state;
private String zipCode;
private String country;
public Address(String street, String city, String state, String zipCode, String country) {
this.street = street;
this.city = city;
this.state = state;
this.zipCode = zipCode;
this.country = country;
}
// Getters and setters
}
Enums
public enum BookStatus {
AVAILABLE,
CHECKED_OUT,
RESERVED,
LOST,
DAMAGED,
REFERENCE_ONLY
}
public enum MembershipStatus {
ACTIVE,
SUSPENDED,
CANCELLED,
EXPIRED
}
public enum MembershipType {
BASIC,
PREMIUM
}
public enum LoanStatus {
ACTIVE,
RETURNED,
OVERDUE,
LOST
}
public enum ReservationStatus {
PENDING,
FULFILLED,
CANCELLED,
EXPIRED
}
Core Service Classes
Library Management Service
public class LibraryService {
private BookRepository bookRepository;
private MemberRepository memberRepository;
private LoanRepository loanRepository;
private ReservationRepository reservationRepository;
private NotificationService notificationService;
public LibraryService() {
this.bookRepository = new BookRepository();
this.memberRepository = new MemberRepository();
this.loanRepository = new LoanRepository();
this.reservationRepository = new ReservationRepository();
this.notificationService = new NotificationService();
}
// Book Management
public boolean addBook(Book book) {
return bookRepository.addBook(book);
}
public List<Book> searchBooks(String query, SearchType searchType) {
return bookRepository.searchBooks(query, searchType);
}
public Book getBookByISBN(String ISBN) {
return bookRepository.getBookByISBN(ISBN);
}
// Member Management
public boolean registerMember(Member member) {
return memberRepository.addMember(member);
}
public Member getMemberById(String memberId) {
return memberRepository.getMemberById(memberId);
}
// Loan Management
public LoanResult checkoutBook(String memberId, String ISBN) {
Member member = memberRepository.getMemberById(memberId);
if (member == null || !member.canBorrowBook()) {
return new LoanResult(false, "Member cannot borrow books");
}
PhysicalBook book = (PhysicalBook) bookRepository.getAvailableBook(ISBN);
if (book == null) {
return new LoanResult(false, "Book not available");
}
// Create loan
String loanId = generateLoanId();
Loan loan = new Loan(loanId, member, book);
// Update book status
book.setStatus(BookStatus.CHECKED_OUT);
// Update member's loans
member.addLoan(loan);
// Save loan
loanRepository.addLoan(loan);
// Send notification
notificationService.notifyBookCheckedOut(member, book, loan.getDueDate());
return new LoanResult(true, "Book checked out successfully", loan);
}
public ReturnResult returnBook(String loanId) {
Loan loan = loanRepository.getLoanById(loanId);
if (loan == null || loan.getStatus() != LoanStatus.ACTIVE) {
return new ReturnResult(false, "Invalid loan");
}
// Process return
loan.returnBook();
loan.getMember().removeLoan(loan);
// Update repository
loanRepository.updateLoan(loan);
// Check for reservations
processReservationQueue(loan.getBook());
// Send notification if late fee
if (loan.getLateFee() > 0) {
notificationService.notifyLateFee(loan.getMember(), loan.getLateFee());
}
return new ReturnResult(true, "Book returned successfully", loan.getLateFee());
}
// Reservation Management
public ReservationResult reserveBook(String memberId, String ISBN) {
Member member = memberRepository.getMemberById(memberId);
if (member == null || !member.canReserveBooks()) {
return new ReservationResult(false, "Member cannot reserve books");
}
Book book = bookRepository.getBookByISBN(ISBN);
if (book == null) {
return new ReservationResult(false, "Book not found");
}
// Check if book is available
if (bookRepository.isBookAvailable(ISBN)) {
return new ReservationResult(false, "Book is currently available for checkout");
}
// Create reservation
String reservationId = generateReservationId();
Reservation reservation = new Reservation(reservationId, member, book);
reservationRepository.addReservation(reservation);
member.getReservations().add(reservation);
notificationService.notifyBookReserved(member, book);
return new ReservationResult(true, "Book reserved successfully", reservation);
}
private void processReservationQueue(Book book) {
List<Reservation> pendingReservations = reservationRepository
.getPendingReservationsByBook(book.getISBN());
if (!pendingReservations.isEmpty()) {
Reservation nextReservation = pendingReservations.get(0);
nextReservation.setStatus(ReservationStatus.FULFILLED);
// Mark book as reserved
if (book instanceof PhysicalBook) {
((PhysicalBook) book).setStatus(BookStatus.RESERVED);
}
// Notify member
notificationService.notifyBookAvailable(
nextReservation.getMember(), book);
}
}
// Utility methods
private String generateLoanId() {
return "LOAN_" + System.currentTimeMillis();
}
private String generateReservationId() {
return "RES_" + System.currentTimeMillis();
}
}
Search Service
public class SearchService {
private BookRepository bookRepository;
public SearchService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public SearchResult searchBooks(SearchCriteria criteria) {
List<Book> results = new ArrayList<>();
// Apply different search strategies based on criteria
if (criteria.hasTitle()) {
results.addAll(bookRepository.searchByTitle(criteria.getTitle()));
}
if (criteria.hasAuthor()) {
results.addAll(bookRepository.searchByAuthor(criteria.getAuthor()));
}
if (criteria.hasISBN()) {
Book book = bookRepository.getBookByISBN(criteria.getISBN());
if (book != null) {
results.add(book);
}
}
if (criteria.hasSubject()) {
results.addAll(bookRepository.searchBySubject(criteria.getSubject()));
}
// Remove duplicates and apply filters
results = results.stream()
.distinct()
.filter(book -> applyFilters(book, criteria))
.collect(Collectors.toList());
// Sort results
results.sort(createComparator(criteria.getSortBy()));
return new SearchResult(results, results.size());
}
private boolean applyFilters(Book book, SearchCriteria criteria) {
if (criteria.hasLanguageFilter() &&
!book.getLanguage().equals(criteria.getLanguage())) {
return false;
}
if (criteria.hasAvailabilityFilter() &&
criteria.isAvailableOnly() &&
!bookRepository.isBookAvailable(book.getISBN())) {
return false;
}
return true;
}
private Comparator<Book> createComparator(SortBy sortBy) {
switch (sortBy) {
case TITLE:
return Comparator.comparing(Book::getTitle);
case AUTHOR:
return (b1, b2) -> b1.getAuthors().get(0).getName()
.compareTo(b2.getAuthors().get(0).getName());
case RELEVANCE:
default:
return (b1, b2) -> 0; // Keep original order
}
}
}
// Supporting classes for search
public class SearchCriteria {
private String title;
private String author;
private String ISBN;
private String subject;
private String language;
private boolean availableOnly;
private SortBy sortBy;
// Builder pattern for easy construction
public static class Builder {
private SearchCriteria criteria = new SearchCriteria();
public Builder title(String title) {
criteria.title = title;
return this;
}
public Builder author(String author) {
criteria.author = author;
return this;
}
public Builder ISBN(String ISBN) {
criteria.ISBN = ISBN;
return this;
}
public Builder subject(String subject) {
criteria.subject = subject;
return this;
}
public Builder availableOnly(boolean availableOnly) {
criteria.availableOnly = availableOnly;
return this;
}
public Builder sortBy(SortBy sortBy) {
criteria.sortBy = sortBy;
return this;
}
public SearchCriteria build() {
return criteria;
}
}
// Getters and helper methods
public boolean hasTitle() { return title != null && !title.trim().isEmpty(); }
public boolean hasAuthor() { return author != null && !author.trim().isEmpty(); }
public boolean hasISBN() { return ISBN != null && !ISBN.trim().isEmpty(); }
public boolean hasSubject() { return subject != null && !subject.trim().isEmpty(); }
public boolean hasLanguageFilter() { return language != null; }
public boolean hasAvailabilityFilter() { return availableOnly; }
// Getters
public String getTitle() { return title; }
public String getAuthor() { return author; }
public String getISBN() { return ISBN; }
public String getSubject() { return subject; }
public String getLanguage() { return language; }
public boolean isAvailableOnly() { return availableOnly; }
public SortBy getSortBy() { return sortBy != null ? sortBy : SortBy.RELEVANCE; }
}
public enum SortBy {
RELEVANCE,
TITLE,
AUTHOR,
PUBLICATION_DATE
}
public class SearchResult {
private List<Book> books;
private int totalCount;
public SearchResult(List<Book> books, int totalCount) {
this.books = books;
this.totalCount = totalCount;
}
public List<Book> getBooks() { return books; }
public int getTotalCount() { return totalCount; }
}
Design Patterns Applied
1. Strategy Pattern - Member Types
Different member types have different borrowing rules, implemented through strategy pattern:
public interface BorrowingStrategy {
int getMaxBooksAllowed();
int getLoanDurationDays();
double getLateFeePerDay();
boolean canReserveBooks();
}
public class StudentBorrowingStrategy implements BorrowingStrategy {
public int getMaxBooksAllowed() { return 5; }
public int getLoanDurationDays() { return 30; }
public double getLateFeePerDay() { return 0.50; }
public boolean canReserveBooks() { return true; }
}
2. Factory Pattern - Member Creation
public class MemberFactory {
public static Member createMember(MemberType type, String memberId,
String name, String email, String phone,
Map<String, String> additionalInfo) {
switch (type) {
case STUDENT:
return new StudentMember(memberId, name, email, phone,
additionalInfo.get("studentId"),
additionalInfo.get("institution"));
case FACULTY:
return new FacultyMember(memberId, name, email, phone,
additionalInfo.get("department"),
additionalInfo.get("employeeId"));
case GENERAL:
MembershipType membershipType = MembershipType.valueOf(
additionalInfo.get("membershipType"));
return new GeneralMember(memberId, name, email, phone, membershipType);
default:
throw new IllegalArgumentException("Unknown member type: " + type);
}
}
}
3. Observer Pattern - Notifications
public interface LibraryEventObserver {
void onBookCheckedOut(Member member, Book book, Date dueDate);
void onBookReturned(Member member, Book book, double lateFee);
void onBookReserved(Member member, Book book);
void onBookAvailable(Member member, Book book);
}
public class NotificationService implements LibraryEventObserver {
private EmailService emailService;
private SMSService smsService;
@Override
public void onBookCheckedOut(Member member, Book book, Date dueDate) {
String message = String.format("Book '%s' checked out. Due date: %s",
book.getTitle(), dueDate);
emailService.sendEmail(member.getEmail(), "Book Checked Out", message);
}
@Override
public void onBookReturned(Member member, Book book, double lateFee) {
if (lateFee > 0) {
String message = String.format("Book '%s' returned. Late fee: $%.2f",
book.getTitle(), lateFee);
emailService.sendEmail(member.getEmail(), "Late Fee Notice", message);
}
}
// Other notification methods...
}
4. Repository Pattern - Data Access
public interface BookRepository {
boolean addBook(Book book);
Book getBookByISBN(String ISBN);
List<Book> searchByTitle(String title);
List<Book> searchByAuthor(String author);
List<Book> searchBySubject(String subject);
boolean isBookAvailable(String ISBN);
PhysicalBook getAvailableBook(String ISBN);
boolean updateBook(Book book);
boolean removeBook(String ISBN);
}
public class InMemoryBookRepository implements BookRepository {
private Map<String, List<Book>> booksByISBN;
private Map<String, List<Book>> booksByTitle;
private Map<String, List<Book>> booksByAuthor;
private Map<String, List<Book>> booksBySubject;
public InMemoryBookRepository() {
this.booksByISBN = new HashMap<>();
this.booksByTitle = new HashMap<>();
this.booksByAuthor = new HashMap<>();
this.booksBySubject = new HashMap<>();
}
@Override
public boolean addBook(Book book) {
// Add to ISBN index
booksByISBN.computeIfAbsent(book.getISBN(), k -> new ArrayList<>()).add(book);
// Add to title index
String titleKey = book.getTitle().toLowerCase();
booksByTitle.computeIfAbsent(titleKey, k -> new ArrayList<>()).add(book);
// Add to author index
for (Author author : book.getAuthors()) {
String authorKey = author.getName().toLowerCase();
booksByAuthor.computeIfAbsent(authorKey, k -> new ArrayList<>()).add(book);
}
// Add to subject index
String subjectKey = book.getSubject().toLowerCase();
booksBySubject.computeIfAbsent(subjectKey, k -> new ArrayList<>()).add(book);
return true;
}
@Override
public List<Book> searchByTitle(String title) {
return booksByTitle.getOrDefault(title.toLowerCase(), new ArrayList<>());
}
@Override
public PhysicalBook getAvailableBook(String ISBN) {
List<Book> books = booksByISBN.get(ISBN);
if (books != null) {
return books.stream()
.filter(book -> book instanceof PhysicalBook)
.map(book -> (PhysicalBook) book)
.filter(book -> book.getStatus() == BookStatus.AVAILABLE)
.findFirst()
.orElse(null);
}
return null;
}
// Other repository methods...
}
5. Builder Pattern - Search Criteria
Already shown in the SearchCriteria class above.
Advanced Features
1. Fine Management System
public class FineService {
private static final double DAILY_FINE_RATE = 1.00;
private static final double MAX_FINE_AMOUNT = 50.00;
public Fine calculateFine(Loan loan) {
if (!loan.isOverdue()) {
return new Fine(0.0, "No fine - book returned on time");
}
long overdueDays = calculateOverdueDays(loan);
double fineAmount = Math.min(
overdueDays * loan.getMember().getLateFeePerDay(),
MAX_FINE_AMOUNT
);
return new Fine(fineAmount,
String.format("Fine for %d days overdue", overdueDays));
}
private long calculateOverdueDays(Loan loan) {
Date currentDate = loan.getReturnDate() != null ?
loan.getReturnDate() : new Date();
return (currentDate.getTime() - loan.getDueDate().getTime()) /
(1000 * 60 * 60 * 24);
}
}
public class Fine {
private double amount;
private String description;
private Date createdDate;
private boolean isPaid;
public Fine(double amount, String description) {
this.amount = amount;
this.description = description;
this.createdDate = new Date();
this.isPaid = false;
}
// Getters and setters
}
2. Book Recommendation System
public class RecommendationService {
private LoanRepository loanRepository;
private BookRepository bookRepository;
public List<Book> getRecommendations(Member member, int count) {
// Get member's borrowing history
List<Loan> memberLoans = loanRepository.getLoansByMember(member.getMemberId());
// Extract subjects/genres from borrowed books
Map<String, Integer> subjectFrequency = memberLoans.stream()
.collect(Collectors.groupingBy(
loan -> loan.getBook().getSubject(),
Collectors.collectingAndThen(Collectors.counting(), Long::intValue)
));
// Find popular books in preferred subjects
List<Book> recommendations = new ArrayList<>();
for (String subject : subjectFrequency.keySet()) {
recommendations.addAll(bookRepository.getPopularBooksBySubject(subject));
}
// Remove already borrowed books
Set<String> borrowedISBNs = memberLoans.stream()
.map(loan -> loan.getBook().getISBN())
.collect(Collectors.toSet());
return recommendations.stream()
.filter(book -> !borrowedISBNs.contains(book.getISBN()))
.limit(count)
.collect(Collectors.toList());
}
}
3. Inventory Management
public class InventoryService {
private BookRepository bookRepository;
public InventoryReport generateInventoryReport() {
Map<String, Integer> totalBooks = new HashMap<>();
Map<String, Integer> availableBooks = new HashMap<>();
Map<String, Integer> checkedOutBooks = new HashMap<>();
// Count books by status
List<Book> allBooks = bookRepository.getAllBooks();
for (Book book : allBooks) {
String isbn = book.getISBN();
totalBooks.merge(isbn, 1, Integer::sum);
if (book instanceof PhysicalBook) {
PhysicalBook physicalBook = (PhysicalBook) book;
if (physicalBook.getStatus() == BookStatus.AVAILABLE) {
availableBooks.merge(isbn, 1, Integer::sum);
} else if (physicalBook.getStatus() == BookStatus.CHECKED_OUT) {
checkedOutBooks.merge(isbn, 1, Integer::sum);
}
}
}
return new InventoryReport(totalBooks, availableBooks, checkedOutBooks);
}
public List<Book> getLowStockBooks(int threshold) {
return bookRepository.getAllUniqueBooks().stream()
.filter(book -> bookRepository.getAvailableCount(book.getISBN()) <= threshold)
.collect(Collectors.toList());
}
}
SOLID Principles Applied
1. Single Responsibility Principle (SRP)
LibraryService
handles library operationsSearchService
handles search functionalityNotificationService
handles notificationsFineService
handles fine calculations
2. Open/Closed Principle (OCP)
- New member types can be added by extending
Member
class - New book types can be added by extending
Book
class - New notification channels can be added by implementing
LibraryEventObserver
3. Liskov Substitution Principle (LSP)
- All member subtypes can be used wherever
Member
is expected - All book subtypes can be used wherever
Book
is expected
4. Interface Segregation Principle (ISP)
- Separate interfaces for different repository operations
- Specific interfaces for different services
5. Dependency Inversion Principle (DIP)
- Services depend on repository interfaces, not concrete implementations
- High-level modules don't depend on low-level modules
Testing Strategy
Unit Tests
public class LibraryServiceTest {
private LibraryService libraryService;
private BookRepository mockBookRepository;
private MemberRepository mockMemberRepository;
@Before
public void setUp() {
mockBookRepository = mock(BookRepository.class);
mockMemberRepository = mock(MemberRepository.class);
libraryService = new LibraryService(mockBookRepository, mockMemberRepository);
}
@Test
public void testCheckoutBook_Success() {
// Arrange
String memberId = "M001";
String isbn = "978-1234567890";
Member member = new StudentMember(memberId, "John Doe", "john@email.com", "1234567890", "S001", "University");
PhysicalBook book = new PhysicalBook(isbn, "Test Book", "Computer Science", "Publisher", "English", 200, "A1-001");
when(mockMemberRepository.getMemberById(memberId)).thenReturn(member);
when(mockBookRepository.getAvailableBook(isbn)).thenReturn(book);
// Act
LoanResult result = libraryService.checkoutBook(memberId, isbn);
// Assert
assertTrue(result.isSuccess());
assertEquals(BookStatus.CHECKED_OUT, book.getStatus());
assertEquals(1, member.getCurrentLoans().size());
}
@Test
public void testCheckoutBook_BookNotAvailable() {
// Arrange
String memberId = "M001";
String isbn = "978-1234567890";
Member member = new StudentMember(memberId, "John Doe", "john@email.com", "1234567890", "S001", "University");
when(mockMemberRepository.getMemberById(memberId)).thenReturn(member);
when(mockBookRepository.getAvailableBook(isbn)).thenReturn(null);
// Act
LoanResult result = libraryService.checkoutBook(memberId, isbn);
// Assert
assertFalse(result.isSuccess());
assertEquals("Book not available", result.getMessage());
}
}
Performance Considerations
1. Indexing Strategy
- Create indexes on frequently searched fields (title, author, ISBN)
- Use composite indexes for complex queries
- Implement caching for popular searches
2. Caching
public class CachedBookRepository implements BookRepository {
private BookRepository delegate;
private Cache<String, Book> bookCache;
private Cache<String, List<Book>> searchCache;
public CachedBookRepository(BookRepository delegate) {
this.delegate = delegate;
this.bookCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
this.searchCache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(15, TimeUnit.MINUTES)
.build();
}
@Override
public Book getBookByISBN(String ISBN) {
return bookCache.get(ISBN, () -> delegate.getBookByISBN(ISBN));
}
@Override
public List<Book> searchByTitle(String title) {
return searchCache.get("title:" + title, () -> delegate.searchByTitle(title));
}
}
3. Pagination
public class PaginatedSearchResult {
private List<Book> books;
private int currentPage;
private int pageSize;
private int totalPages;
private long totalResults;
// Constructor and getters
}
public PaginatedSearchResult searchBooks(SearchCriteria criteria, int page, int pageSize) {
int offset = (page - 1) * pageSize;
List<Book> allResults = performSearch(criteria);
long totalResults = allResults.size();
int totalPages = (int) Math.ceil((double) totalResults / pageSize);
List<Book> pageResults = allResults.stream()
.skip(offset)
.limit(pageSize)
.collect(Collectors.toList());
return new PaginatedSearchResult(pageResults, page, pageSize, totalPages, totalResults);
}
Extensibility & Future Enhancements
1. Digital Rights Management
public class DRMService {
public boolean canAccessDigitalBook(Member member, DigitalBook book) {
// Check licensing restrictions
// Verify member's digital access rights
// Handle concurrent user limits
return true;
}
public DigitalAccessToken generateAccessToken(Member member, DigitalBook book) {
// Generate time-limited access token
return new DigitalAccessToken(member, book, Duration.ofDays(14));
}
}
2. Integration Points
public interface ExternalLibraryService {
List<Book> searchExternalCatalog(String query);
boolean requestInterlibrary
Loan(String isbn, String targetLibrary);
}
public interface PaymentService {
PaymentResult processFinePayment(Member member, double amount);
PaymentResult processLostBookPayment(Member member, Book book);
}
3. Analytics & Reporting
public class AnalyticsService {
public PopularityReport generatePopularityReport(Date startDate, Date endDate) {
// Track most borrowed books
// Analyze borrowing patterns
// Generate insights
return new PopularityReport();
}
public MembershipReport generateMembershipReport() {
// Track member activity
// Analyze membership trends
return new MembershipReport();
}
}
Interview Discussion Points
Key Design Decisions
- Why use inheritance for Member types? - Different borrowing rules and privileges
- Repository pattern benefits - Separation of concerns, testability, swappable implementations
- Strategy pattern for member rules - Easy to add new member types with different rules
- Observer pattern for notifications - Loose coupling, extensible notification system
Scalability Considerations
- Database design - Proper indexing, normalization, partitioning strategies
- Caching strategy - Multi-level caching for frequently accessed data
- Search optimization - Elasticsearch integration for full-text search
- Microservices - Split into separate services (Book Service, Member Service, Loan Service)
Trade-offs Discussed
- In-memory vs Database - Started with in-memory for simplicity, easy to migrate
- Real-time vs Eventual consistency - Chose consistency for financial operations
- Complexity vs Flexibility - Balanced with interfaces and design patterns
This comprehensive library management system demonstrates solid object-oriented design principles, proper use of design patterns, and consideration for real-world requirements like scalability and maintainability.