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 operations
  • SearchService handles search functionality
  • NotificationService handles notifications
  • FineService 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

  1. Why use inheritance for Member types? - Different borrowing rules and privileges
  2. Repository pattern benefits - Separation of concerns, testability, swappable implementations
  3. Strategy pattern for member rules - Easy to add new member types with different rules
  4. Observer pattern for notifications - Loose coupling, extensible notification system

Scalability Considerations

  1. Database design - Proper indexing, normalization, partitioning strategies
  2. Caching strategy - Multi-level caching for frequently accessed data
  3. Search optimization - Elasticsearch integration for full-text search
  4. Microservices - Split into separate services (Book Service, Member Service, Loan Service)

Trade-offs Discussed

  1. In-memory vs Database - Started with in-memory for simplicity, easy to migrate
  2. Real-time vs Eventual consistency - Chose consistency for financial operations
  3. 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.