Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
โฑ๏ธ 30 min read

Designing a Library Management System

Difficulty: Beginner Patterns: Strategy, Observer, Factory Asked at: Flipkart, TCS, Infosys, Wipro, Goldman Sachs


Functional Requirements

  1. Add/search books - add books to catalog, search by title, author, or ISBN
  2. Issue/return books - members can borrow and return books with due dates
  3. Fine calculation - compute overdue fines using configurable strategies (per day, flat, tiered)
  4. Reservation queue - when a book is unavailable, members can reserve and get notified when returned
  5. Member management - register members, track borrowing history, set borrowing limits
  6. Overdue tracking - identify overdue books and notify members

Non-Functional Requirements

  1. Thread-safety - concurrent issue/return operations must not corrupt book state
  2. Extensibility - new fine strategies, member tiers, or book categories with minimal changes
  3. Fast search - book lookup by ISBN in O(1), search by title/author efficiently

Core Entities

Entity Description
Book ISBN, title, author, category, total copies
BookCopy Individual physical copy of a book with availability status
Member ID, name, membership type, borrowed books list
MemberType Enum: STANDARD, PREMIUM (different borrowing limits)
BookLoan Links member to book copy with issue date, due date, return date
Reservation Member waiting for a specific book
FineStrategy Interface for computing overdue fines
Library Central system managing all operations
LibraryObserver Notified on issue, return, overdue events

Class Diagram

classDiagram
    class MemberType {
        <<enumeration>>
        STANDARD
        PREMIUM
    }

    class BookStatus {
        <<enumeration>>
        AVAILABLE
        ISSUED
        RESERVED
        LOST
    }

    class Book {
        -String isbn
        -String title
        -String author
        -String category
        -List~BookCopy~ copies
        +getAvailableCopy() BookCopy
        +getTotalCopies() int
        +getAvailableCount() int
    }

    class BookCopy {
        -String copyId
        -Book book
        -BookStatus status
        +isAvailable() boolean
        +markIssued()
        +markReturned()
    }

    class Member {
        -String id
        -String name
        -MemberType type
        -List~BookLoan~ activeLoans
        -int maxBooks
        +canBorrow() boolean
        +getBorrowedCount() int
    }

    class BookLoan {
        -String id
        -Member member
        -BookCopy bookCopy
        -LocalDate issueDate
        -LocalDate dueDate
        -LocalDate returnDate
        -double fineAmount
        +isOverdue() boolean
        +getDaysOverdue() int
    }

    class Reservation {
        -Member member
        -Book book
        -LocalDateTime reservedAt
    }

    class FineStrategy {
        <<interface>>
        +calculateFine(int daysOverdue) double
        +getDescription() String
    }

    class PerDayFine {
        -double ratePerDay
        +calculateFine(int days) double
    }

    class TieredFine {
        +calculateFine(int days) double
    }

    class FlatFine {
        -double amount
        +calculateFine(int days) double
    }

    class LibraryObserver {
        <<interface>>
        +onBookIssued(BookLoan loan)
        +onBookReturned(BookLoan loan)
        +onBookOverdue(BookLoan loan)
        +onReservationAvailable(Reservation res)
    }

    class Library {
        -Map~String, Book~ catalog
        -Map~String, Member~ members
        -Map~String, BookLoan~ activeLoans
        -Map~String, Queue~Reservation~~ reservations
        -FineStrategy fineStrategy
        -List~LibraryObserver~ observers
        +addBook(Book)
        +registerMember(Member)
        +issueBook(String memberId, String isbn) BookLoan
        +returnBook(String loanId) double
        +searchByTitle(String title) List
        +searchByAuthor(String author) List
        +reserveBook(String memberId, String isbn)
        +getOverdueLoans() List
    }

    Library --> Book
    Library --> Member
    Library --> BookLoan
    Library --> FineStrategy
    Library --> LibraryObserver
    Book --> BookCopy
    BookCopy --> BookStatus
    BookLoan --> Member
    BookLoan --> BookCopy
    Member --> MemberType
    FineStrategy <|.. PerDayFine
    FineStrategy <|.. TieredFine
    FineStrategy <|.. FlatFine

Design Patterns

Pattern Where Why
Strategy FineStrategy with PerDay/Tiered/Flat implementations Swap fine calculation logic without touching Library code
Observer LibraryObserver notified on issue/return/overdue Decouple notifications, analytics from core library logic
Factory Member creation with type-based defaults STANDARD gets 3 books limit, PREMIUM gets 10

Data Structures

Component Structure Why
Book catalog HashMap<String, Book> keyed by ISBN O(1) lookup by ISBN
Title search Linear scan on catalog values Acceptable for interview; real system uses full-text index
Members HashMap<String, Member> O(1) member lookup
Active loans HashMap<String, BookLoan> O(1) loan lookup for returns
Reservations Map<String, Queue<Reservation>> per ISBN FIFO reservation; first to reserve gets book first
Memberโ€™s loans ArrayList<BookLoan> per member Track active borrows and history

How It All Fits Together

Hereโ€™s the complete flow for issuing and returning a book:

Issue flow:

  1. Member requests book โ†’ issueBook(memberId, isbn)
  2. Library checks: member exists, member hasnโ€™t exceeded borrowing limit
  3. Finds book by ISBN, gets an available copy
  4. If no copy available: offer reservation
  5. Creates BookLoan with issue date and due date (14 days for STANDARD, 30 for PREMIUM)
  6. Marks copy as ISSUED, adds loan to memberโ€™s active loans
  7. Notifies observers

Return flow:

  1. Member returns book โ†’ returnBook(loanId)
  2. Library finds the loan, calculates overdue days
  3. FineStrategy computes fine if overdue
  4. Marks copy as AVAILABLE, sets return date on loan
  5. Checks reservation queue: if someone waiting, auto-issue to first in queue
  6. Notifies observers of return (and reservation fulfillment if applicable)

Complete Code

All Classes in Single File

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
import java.time.*;
import java.time.format.*;
import java.util.stream.*;

// โ”€โ”€โ”€ Enums โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
enum MemberType {
    STANDARD(3, 14),   // 3 books max, 14 day loan period
    PREMIUM(10, 30);   // 10 books max, 30 day loan period

    private final int maxBooks;
    private final int loanDays;

    MemberType(int maxBooks, int loanDays) {
        this.maxBooks = maxBooks;
        this.loanDays = loanDays;
    }

    public int getMaxBooks() { return maxBooks; }
    public int getLoanDays() { return loanDays; }
}

enum BookStatus { AVAILABLE, ISSUED, RESERVED, LOST }

// โ”€โ”€โ”€ Book Copy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class BookCopy {
    private final String copyId;
    private final String isbn; // reference to parent book
    private BookStatus status;

    public BookCopy(String copyId, String isbn) {
        this.copyId = copyId;
        this.isbn = isbn;
        this.status = BookStatus.AVAILABLE;
    }

    public boolean isAvailable() { return status == BookStatus.AVAILABLE; }
    public void markIssued() { status = BookStatus.ISSUED; }
    public void markReturned() { status = BookStatus.AVAILABLE; }
    public void markLost() { status = BookStatus.LOST; }

    public String getCopyId() { return copyId; }
    public String getIsbn() { return isbn; }
    public BookStatus getStatus() { return status; }
}

// โ”€โ”€โ”€ Book โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class Book {
    private final String isbn;
    private final String title;
    private final String author;
    private final String category;
    private final List<BookCopy> copies;

    public Book(String isbn, String title, String author, String category, int numCopies) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.category = category;
        this.copies = new ArrayList<>();
        for (int i = 1; i <= numCopies; i++) {
            copies.add(new BookCopy(isbn + "-C" + i, isbn));
        }
    }

    public BookCopy getAvailableCopy() {
        return copies.stream().filter(BookCopy::isAvailable).findFirst().orElse(null);
    }

    public int getAvailableCount() {
        return (int) copies.stream().filter(BookCopy::isAvailable).count();
    }

    public int getTotalCopies() { return copies.size(); }
    public String getIsbn() { return isbn; }
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public String getCategory() { return category; }

    @Override
    public String toString() {
        return "Book[" + title + " by " + author + " | ISBN=" + isbn +
               " | " + getAvailableCount() + "/" + copies.size() + " available]";
    }
}

// โ”€โ”€โ”€ Member โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class Member {
    private final String id;
    private final String name;
    private final MemberType type;
    private final List<String> activeLoanIds;

    public Member(String id, String name, MemberType type) {
        this.id = id;
        this.name = name;
        this.type = type;
        this.activeLoanIds = new ArrayList<>();
    }

    public boolean canBorrow() {
        return activeLoanIds.size() < type.getMaxBooks();
    }

    public void addLoan(String loanId) { activeLoanIds.add(loanId); }
    public void removeLoan(String loanId) { activeLoanIds.remove(loanId); }
    public int getBorrowedCount() { return activeLoanIds.size(); }

    public String getId() { return id; }
    public String getName() { return name; }
    public MemberType getType() { return type; }
    public int getMaxBooks() { return type.getMaxBooks(); }

    @Override
    public String toString() {
        return "Member[" + name + " | " + type + " | borrowed=" +
               activeLoanIds.size() + "/" + type.getMaxBooks() + "]";
    }
}

// โ”€โ”€โ”€ Book Loan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class BookLoan {
    private final String id;
    private final Member member;
    private final BookCopy bookCopy;
    private final String bookTitle;
    private final LocalDate issueDate;
    private final LocalDate dueDate;
    private LocalDate returnDate;
    private double fineAmount;

    public BookLoan(Member member, BookCopy bookCopy, String bookTitle) {
        this.id = "LOAN-" + UUID.randomUUID().toString().substring(0, 6);
        this.member = member;
        this.bookCopy = bookCopy;
        this.bookTitle = bookTitle;
        this.issueDate = LocalDate.now();
        this.dueDate = issueDate.plusDays(member.getType().getLoanDays());
        this.fineAmount = 0;
    }

    // Constructor with custom dates for testing
    public BookLoan(Member member, BookCopy bookCopy, String bookTitle,
                    LocalDate issueDate, LocalDate dueDate) {
        this.id = "LOAN-" + UUID.randomUUID().toString().substring(0, 6);
        this.member = member;
        this.bookCopy = bookCopy;
        this.bookTitle = bookTitle;
        this.issueDate = issueDate;
        this.dueDate = dueDate;
        this.fineAmount = 0;
    }

    public boolean isOverdue() {
        LocalDate checkDate = returnDate != null ? returnDate : LocalDate.now();
        return checkDate.isAfter(dueDate);
    }

    public int getDaysOverdue() {
        if (!isOverdue()) return 0;
        LocalDate checkDate = returnDate != null ? returnDate : LocalDate.now();
        return (int) dueDate.until(checkDate).getDays();
    }

    public void markReturned(double fine) {
        this.returnDate = LocalDate.now();
        this.fineAmount = fine;
    }

    public void markReturnedOn(LocalDate date, double fine) {
        this.returnDate = date;
        this.fineAmount = fine;
    }

    public String getId() { return id; }
    public Member getMember() { return member; }
    public BookCopy getBookCopy() { return bookCopy; }
    public LocalDate getIssueDate() { return issueDate; }
    public LocalDate getDueDate() { return dueDate; }
    public LocalDate getReturnDate() { return returnDate; }
    public double getFineAmount() { return fineAmount; }
    public String getBookTitle() { return bookTitle; }

    @Override
    public String toString() {
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd-MMM");
        String status = returnDate != null ? "RETURNED" : (isOverdue() ? "OVERDUE" : "ACTIVE");
        return "Loan[" + id + " | " + bookTitle + " | " + member.getName() +
               " | due=" + dueDate.format(fmt) + " | " + status +
               (fineAmount > 0 ? " | fine=โ‚น" + String.format("%.2f", fineAmount) : "") + "]";
    }
}

// โ”€โ”€โ”€ Fine Strategy Interface โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
interface FineStrategy {
    double calculateFine(int daysOverdue);
    String getDescription();
}

// โ”€โ”€โ”€ Per Day Fine โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class PerDayFine implements FineStrategy {
    private final double ratePerDay;

    public PerDayFine(double ratePerDay) {
        this.ratePerDay = ratePerDay;
    }

    @Override
    public double calculateFine(int daysOverdue) {
        return daysOverdue * ratePerDay;
    }

    @Override
    public String getDescription() {
        return "โ‚น" + ratePerDay + " per day";
    }
}

// โ”€โ”€โ”€ Tiered Fine โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class TieredFine implements FineStrategy {
    @Override
    public double calculateFine(int daysOverdue) {
        if (daysOverdue <= 0) return 0;
        if (daysOverdue <= 7) return daysOverdue * 2.0;     // โ‚น2/day first week
        if (daysOverdue <= 14) return 14 + (daysOverdue - 7) * 5.0; // โ‚น5/day second week
        return 14 + 35 + (daysOverdue - 14) * 10.0;         // โ‚น10/day after
    }

    @Override
    public String getDescription() {
        return "Tiered: โ‚น2/day (week 1), โ‚น5/day (week 2), โ‚น10/day (after)";
    }
}

// โ”€โ”€โ”€ Flat Fine โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class FlatFine implements FineStrategy {
    private final double amount;

    public FlatFine(double amount) {
        this.amount = amount;
    }

    @Override
    public double calculateFine(int daysOverdue) {
        return daysOverdue > 0 ? amount : 0;
    }

    @Override
    public String getDescription() {
        return "Flat โ‚น" + amount + " for any overdue";
    }
}

// โ”€โ”€โ”€ Reservation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class Reservation {
    private final Member member;
    private final String isbn;
    private final LocalDateTime reservedAt;

    public Reservation(Member member, String isbn) {
        this.member = member;
        this.isbn = isbn;
        this.reservedAt = LocalDateTime.now();
    }

    public Member getMember() { return member; }
    public String getIsbn() { return isbn; }
    public LocalDateTime getReservedAt() { return reservedAt; }
}

// โ”€โ”€โ”€ Library Observer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
interface LibraryObserver {
    void onBookIssued(BookLoan loan);
    void onBookReturned(BookLoan loan);
    void onBookOverdue(BookLoan loan);
    void onReservationAvailable(Member member, String bookTitle);
}

class LoggingLibraryObserver implements LibraryObserver {
    @Override
    public void onBookIssued(BookLoan loan) {
        System.out.println("  [ISSUED] " + loan.getBookTitle() + " to " + loan.getMember().getName() +
                          " (due: " + loan.getDueDate() + ")");
    }
    @Override
    public void onBookReturned(BookLoan loan) {
        System.out.println("  [RETURNED] " + loan.getBookTitle() + " by " + loan.getMember().getName() +
                          (loan.getFineAmount() > 0 ? " | Fine: โ‚น" + String.format("%.2f", loan.getFineAmount()) : ""));
    }
    @Override
    public void onBookOverdue(BookLoan loan) {
        System.out.println("  [OVERDUE] " + loan.getBookTitle() + " - " + loan.getMember().getName() +
                          " (" + loan.getDaysOverdue() + " days late)");
    }
    @Override
    public void onReservationAvailable(Member member, String bookTitle) {
        System.out.println("  [RESERVED] " + bookTitle + " is now available for " + member.getName());
    }
}

// โ”€โ”€โ”€ Library โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class Library {
    private final ConcurrentHashMap<String, Book> catalog; // isbn -> book
    private final ConcurrentHashMap<String, Member> members; // memberId -> member
    private final ConcurrentHashMap<String, BookLoan> activeLoans; // loanId -> loan
    private final ConcurrentHashMap<String, Queue<Reservation>> reservations; // isbn -> queue
    private FineStrategy fineStrategy;
    private final List<LibraryObserver> observers;
    private final ReentrantLock lock;

    public Library(FineStrategy fineStrategy) {
        this.catalog = new ConcurrentHashMap<>();
        this.members = new ConcurrentHashMap<>();
        this.activeLoans = new ConcurrentHashMap<>();
        this.reservations = new ConcurrentHashMap<>();
        this.fineStrategy = fineStrategy;
        this.observers = new CopyOnWriteArrayList<>();
        this.lock = new ReentrantLock();
    }

    public void addObserver(LibraryObserver observer) { observers.add(observer); }
    public void setFineStrategy(FineStrategy strategy) { this.fineStrategy = strategy; }

    public void addBook(Book book) { catalog.put(book.getIsbn(), book); }
    public void registerMember(Member member) { members.put(member.getId(), member); }

    public BookLoan issueBook(String memberId, String isbn) {
        lock.lock();
        try {
            Member member = members.get(memberId);
            if (member == null) throw new RuntimeException("Member not found: " + memberId);
            if (!member.canBorrow()) {
                throw new RuntimeException(member.getName() + " has reached borrowing limit (" +
                    member.getMaxBooks() + " books)");
            }

            Book book = catalog.get(isbn);
            if (book == null) throw new RuntimeException("Book not found: " + isbn);

            BookCopy copy = book.getAvailableCopy();
            if (copy == null) {
                throw new RuntimeException("No copies available for: " + book.getTitle() +
                    ". Use reserveBook() to join waitlist.");
            }

            copy.markIssued();
            BookLoan loan = new BookLoan(member, copy, book.getTitle());
            activeLoans.put(loan.getId(), loan);
            member.addLoan(loan.getId());

            observers.forEach(o -> o.onBookIssued(loan));
            return loan;
        } finally {
            lock.unlock();
        }
    }

    public double returnBook(String loanId) {
        lock.lock();
        try {
            BookLoan loan = activeLoans.get(loanId);
            if (loan == null) throw new RuntimeException("Loan not found: " + loanId);

            int daysOverdue = loan.getDaysOverdue();
            double fine = fineStrategy.calculateFine(daysOverdue);
            loan.markReturned(fine);
            loan.getBookCopy().markReturned();
            loan.getMember().removeLoan(loanId);
            activeLoans.remove(loanId);

            observers.forEach(o -> o.onBookReturned(loan));

            // Check reservations
            String isbn = loan.getBookCopy().getIsbn();
            Queue<Reservation> queue = reservations.get(isbn);
            if (queue != null && !queue.isEmpty()) {
                Reservation res = queue.poll();
                Book book = catalog.get(isbn);
                observers.forEach(o -> o.onReservationAvailable(res.getMember(), book.getTitle()));
            }

            return fine;
        } finally {
            lock.unlock();
        }
    }

    // Return with specific date (for testing overdue scenarios)
    public double returnBookOn(String loanId, LocalDate returnDate) {
        lock.lock();
        try {
            BookLoan loan = activeLoans.get(loanId);
            if (loan == null) throw new RuntimeException("Loan not found: " + loanId);

            int daysOverdue = 0;
            if (returnDate.isAfter(loan.getDueDate())) {
                daysOverdue = (int) loan.getDueDate().until(returnDate).getDays();
            }
            double fine = fineStrategy.calculateFine(daysOverdue);
            loan.markReturnedOn(returnDate, fine);
            loan.getBookCopy().markReturned();
            loan.getMember().removeLoan(loanId);
            activeLoans.remove(loanId);

            observers.forEach(o -> o.onBookReturned(loan));

            String isbn = loan.getBookCopy().getIsbn();
            Queue<Reservation> queue = reservations.get(isbn);
            if (queue != null && !queue.isEmpty()) {
                Reservation res = queue.poll();
                Book book = catalog.get(isbn);
                observers.forEach(o -> o.onReservationAvailable(res.getMember(), book.getTitle()));
            }

            return fine;
        } finally {
            lock.unlock();
        }
    }

    public void reserveBook(String memberId, String isbn) {
        lock.lock();
        try {
            Member member = members.get(memberId);
            if (member == null) throw new RuntimeException("Member not found");
            Book book = catalog.get(isbn);
            if (book == null) throw new RuntimeException("Book not found");

            if (book.getAvailableCount() > 0) {
                throw new RuntimeException("Book is available - issue it directly");
            }

            reservations.computeIfAbsent(isbn, k -> new LinkedList<>())
                       .offer(new Reservation(member, isbn));
            System.out.println("  " + member.getName() + " reserved '" + book.getTitle() + "'");
        } finally {
            lock.unlock();
        }
    }

    public List<Book> searchByTitle(String titleQuery) {
        String query = titleQuery.toLowerCase();
        return catalog.values().stream()
            .filter(b -> b.getTitle().toLowerCase().contains(query))
            .collect(Collectors.toList());
    }

    public List<Book> searchByAuthor(String authorQuery) {
        String query = authorQuery.toLowerCase();
        return catalog.values().stream()
            .filter(b -> b.getAuthor().toLowerCase().contains(query))
            .collect(Collectors.toList());
    }

    public Book searchByIsbn(String isbn) {
        return catalog.get(isbn);
    }

    public List<BookLoan> getOverdueLoans() {
        return activeLoans.values().stream()
            .filter(BookLoan::isOverdue)
            .collect(Collectors.toList());
    }

    public void displayCatalog() {
        System.out.println("\n--- Library Catalog ---");
        catalog.values().forEach(b -> System.out.println("  " + b));
    }

    public void displayMembers() {
        System.out.println("\n--- Members ---");
        members.values().forEach(m -> System.out.println("  " + m));
    }
}

// โ”€โ”€โ”€ Main Demo โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
public class LibraryManagementDemo {
    public static void main(String[] args) {
        System.out.println("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
        System.out.println("   LIBRARY MANAGEMENT - LLD DEMO      ");
        System.out.println("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n");

        // Setup library with tiered fine strategy
        Library library = new Library(new TieredFine());
        library.addObserver(new LoggingLibraryObserver());

        // โ”€โ”€โ”€ Add Books โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        System.out.println("--- Adding Books ---");
        library.addBook(new Book("978-0-13-468599-1", "Clean Code", "Robert C. Martin", "Programming", 3));
        library.addBook(new Book("978-0-201-63361-0", "Design Patterns", "Gang of Four", "Programming", 2));
        library.addBook(new Book("978-0-596-51774-8", "JavaScript: The Good Parts", "Douglas Crockford", "Programming", 1));
        library.addBook(new Book("978-0-06-112008-4", "To Kill a Mockingbird", "Harper Lee", "Fiction", 4));
        library.addBook(new Book("978-0-14-028329-7", "1984", "George Orwell", "Fiction", 2));
        library.displayCatalog();

        // โ”€โ”€โ”€ Register Members โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        System.out.println("\n--- Registering Members ---");
        library.registerMember(new Member("M1", "Rahul", MemberType.STANDARD));
        library.registerMember(new Member("M2", "Priya", MemberType.PREMIUM));
        library.registerMember(new Member("M3", "Arjun", MemberType.STANDARD));
        library.displayMembers();

        // โ”€โ”€โ”€ Issue Books โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        System.out.println("\n--- Issuing Books ---");
        BookLoan loan1 = library.issueBook("M1", "978-0-13-468599-1"); // Clean Code
        BookLoan loan2 = library.issueBook("M2", "978-0-13-468599-1"); // Clean Code (2nd copy)
        BookLoan loan3 = library.issueBook("M1", "978-0-06-112008-4"); // Mockingbird
        BookLoan loan4 = library.issueBook("M3", "978-0-596-51774-8"); // JS Good Parts (only 1 copy)

        // โ”€โ”€โ”€ Search Books โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        System.out.println("\n--- Search Results ---");
        System.out.println("Search 'clean':");
        library.searchByTitle("clean").forEach(b -> System.out.println("  " + b));
        System.out.println("Search by author 'orwell':");
        library.searchByAuthor("orwell").forEach(b -> System.out.println("  " + b));

        // โ”€โ”€โ”€ Reservation (book unavailable) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        System.out.println("\n--- Reservation ---");
        // JS Good Parts has only 1 copy and it's issued to Arjun
        try {
            library.issueBook("M2", "978-0-596-51774-8");
        } catch (RuntimeException e) {
            System.out.println("  " + e.getMessage());
        }
        library.reserveBook("M2", "978-0-596-51774-8");

        // โ”€โ”€โ”€ Return Books (on time) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        System.out.println("\n--- Returning Books (on time) ---");
        double fine1 = library.returnBook(loan1.getId());
        System.out.println("  Fine for Clean Code: โ‚น" + fine1);

        // โ”€โ”€โ”€ Return with Overdue (simulate) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        System.out.println("\n--- Returning Overdue Book (10 days late) ---");
        LocalDate overdueReturn = loan3.getDueDate().plusDays(10);
        double fine3 = library.returnBookOn(loan3.getId(), overdueReturn);
        System.out.println("  Fine for Mockingbird (10 days overdue): โ‚น" + String.format("%.2f", fine3));

        // โ”€โ”€โ”€ Return JS book - triggers reservation notification
        System.out.println("\n--- Returning JS Book (triggers reservation) ---");
        library.returnBook(loan4.getId());

        // โ”€โ”€โ”€ Borrowing Limit โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        System.out.println("\n--- Borrowing Limit Test ---");
        // Rahul is STANDARD (max 3), currently has 0 active
        library.issueBook("M1", "978-0-201-63361-0");
        library.issueBook("M1", "978-0-14-028329-7");
        library.issueBook("M1", "978-0-06-112008-4");
        try {
            library.issueBook("M1", "978-0-13-468599-1");
        } catch (RuntimeException e) {
            System.out.println("  Expected: " + e.getMessage());
        }

        // โ”€โ”€โ”€ Switch Fine Strategy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        System.out.println("\n--- Switch to Per-Day Fine (โ‚น5/day) ---");
        library.setFineStrategy(new PerDayFine(5.0));
        System.out.println("  Fine for 10 days: โ‚น" + new PerDayFine(5.0).calculateFine(10));

        System.out.println("\n--- Fine Strategy Comparison ---");
        System.out.println("  Per-day (โ‚น5):  10 days = โ‚น" + new PerDayFine(5).calculateFine(10));
        System.out.println("  Tiered:        10 days = โ‚น" + new TieredFine().calculateFine(10));
        System.out.println("  Flat (โ‚น50):    10 days = โ‚น" + new FlatFine(50).calculateFine(10));

        library.displayCatalog();
        library.displayMembers();

        System.out.println("\nโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
        System.out.println("           DEMO COMPLETE               ");
        System.out.println("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
    }
}
import threading
import uuid
from datetime import date, datetime, timedelta
from collections import deque
from abc import ABC, abstractmethod
from enum import Enum
from typing import Optional

# โ”€โ”€โ”€ Enums โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class MemberType(Enum):
    STANDARD = ("STANDARD", 3, 14)   # max 3 books, 14 day loan
    PREMIUM = ("PREMIUM", 10, 30)    # max 10 books, 30 day loan

    def __init__(self, label, max_books, loan_days):
        self._label = label
        self._max_books = max_books
        self._loan_days = loan_days

    @property
    def max_books(self): return self._max_books
    @property
    def loan_days(self): return self._loan_days

class BookStatus(Enum):
    AVAILABLE = "AVAILABLE"
    ISSUED = "ISSUED"
    RESERVED = "RESERVED"
    LOST = "LOST"

# โ”€โ”€โ”€ Book Copy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class BookCopy:
    def __init__(self, copy_id: str, isbn: str):
        self.copy_id = copy_id
        self.isbn = isbn
        self.status = BookStatus.AVAILABLE

    @property
    def is_available(self) -> bool:
        return self.status == BookStatus.AVAILABLE

    def mark_issued(self): self.status = BookStatus.ISSUED
    def mark_returned(self): self.status = BookStatus.AVAILABLE

# โ”€โ”€โ”€ Book โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class Book:
    def __init__(self, isbn: str, title: str, author: str, category: str, num_copies: int):
        self.isbn = isbn
        self.title = title
        self.author = author
        self.category = category
        self.copies = [BookCopy(f"{isbn}-C{i+1}", isbn) for i in range(num_copies)]

    def get_available_copy(self) -> Optional[BookCopy]:
        for copy in self.copies:
            if copy.is_available:
                return copy
        return None

    @property
    def available_count(self) -> int:
        return sum(1 for c in self.copies if c.is_available)

    def __str__(self) -> str:
        return (f"Book[{self.title} by {self.author} | ISBN={self.isbn} | "
                f"{self.available_count}/{len(self.copies)} available]")

# โ”€โ”€โ”€ Member โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class Member:
    def __init__(self, member_id: str, name: str, member_type: MemberType):
        self.id = member_id
        self.name = name
        self.type = member_type
        self._active_loan_ids: list[str] = []

    def can_borrow(self) -> bool:
        return len(self._active_loan_ids) < self.type.max_books

    def add_loan(self, loan_id: str): self._active_loan_ids.append(loan_id)
    def remove_loan(self, loan_id: str): self._active_loan_ids.remove(loan_id)

    @property
    def borrowed_count(self) -> int: return len(self._active_loan_ids)

    def __str__(self) -> str:
        return (f"Member[{self.name} | {self.type.name} | "
                f"borrowed={self.borrowed_count}/{self.type.max_books}]")

# โ”€โ”€โ”€ Book Loan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class BookLoan:
    def __init__(self, member: Member, book_copy: BookCopy, book_title: str,
                 issue_date: Optional[date] = None):
        self.id = f"LOAN-{uuid.uuid4().hex[:6]}"
        self.member = member
        self.book_copy = book_copy
        self.book_title = book_title
        self.issue_date = issue_date or date.today()
        self.due_date = self.issue_date + timedelta(days=member.type.loan_days)
        self.return_date: Optional[date] = None
        self.fine_amount = 0.0

    @property
    def is_overdue(self) -> bool:
        check = self.return_date or date.today()
        return check > self.due_date

    @property
    def days_overdue(self) -> int:
        if not self.is_overdue:
            return 0
        check = self.return_date or date.today()
        return (check - self.due_date).days

    def mark_returned(self, fine: float, return_date: Optional[date] = None):
        self.return_date = return_date or date.today()
        self.fine_amount = fine

    def __str__(self) -> str:
        status = "RETURNED" if self.return_date else ("OVERDUE" if self.is_overdue else "ACTIVE")
        fine_str = f" | fine=Rs{self.fine_amount:.2f}" if self.fine_amount > 0 else ""
        return (f"Loan[{self.id} | {self.book_title} | {self.member.name} | "
                f"due={self.due_date} | {status}{fine_str}]")

# โ”€โ”€โ”€ Fine Strategy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class FineStrategy(ABC):
    @abstractmethod
    def calculate_fine(self, days_overdue: int) -> float: pass
    @abstractmethod
    def description(self) -> str: pass

class PerDayFine(FineStrategy):
    def __init__(self, rate_per_day: float):
        self._rate = rate_per_day

    def calculate_fine(self, days_overdue: int) -> float:
        return days_overdue * self._rate

    def description(self) -> str:
        return f"Rs{self._rate} per day"

class TieredFine(FineStrategy):
    def calculate_fine(self, days_overdue: int) -> float:
        if days_overdue <= 0: return 0
        if days_overdue <= 7: return days_overdue * 2.0
        if days_overdue <= 14: return 14 + (days_overdue - 7) * 5.0
        return 14 + 35 + (days_overdue - 14) * 10.0

    def description(self) -> str:
        return "Tiered: Rs2/day (week1), Rs5/day (week2), Rs10/day (after)"

class FlatFine(FineStrategy):
    def __init__(self, amount: float):
        self._amount = amount

    def calculate_fine(self, days_overdue: int) -> float:
        return self._amount if days_overdue > 0 else 0

    def description(self) -> str:
        return f"Flat Rs{self._amount}"

# โ”€โ”€โ”€ Reservation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class Reservation:
    def __init__(self, member: Member, isbn: str):
        self.member = member
        self.isbn = isbn
        self.reserved_at = datetime.now()

# โ”€โ”€โ”€ Library Observer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class LibraryObserver(ABC):
    @abstractmethod
    def on_book_issued(self, loan: BookLoan): pass
    @abstractmethod
    def on_book_returned(self, loan: BookLoan): pass
    @abstractmethod
    def on_book_overdue(self, loan: BookLoan): pass
    @abstractmethod
    def on_reservation_available(self, member: Member, book_title: str): pass

class LoggingObserver(LibraryObserver):
    def on_book_issued(self, loan):
        print(f"  [ISSUED] {loan.book_title} to {loan.member.name} (due: {loan.due_date})")
    def on_book_returned(self, loan):
        fine = f" | Fine: Rs{loan.fine_amount:.2f}" if loan.fine_amount > 0 else ""
        print(f"  [RETURNED] {loan.book_title} by {loan.member.name}{fine}")
    def on_book_overdue(self, loan):
        print(f"  [OVERDUE] {loan.book_title} - {loan.member.name} ({loan.days_overdue} days)")
    def on_reservation_available(self, member, book_title):
        print(f"  [RESERVED] {book_title} now available for {member.name}")

# โ”€โ”€โ”€ Library โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class Library:
    def __init__(self, fine_strategy: FineStrategy):
        self._catalog: dict[str, Book] = {}
        self._members: dict[str, Member] = {}
        self._active_loans: dict[str, BookLoan] = {}
        self._reservations: dict[str, deque[Reservation]] = {}
        self._fine_strategy = fine_strategy
        self._observers: list[LibraryObserver] = []
        self._lock = threading.Lock()

    def add_observer(self, obs: LibraryObserver): self._observers.append(obs)
    def set_fine_strategy(self, s: FineStrategy): self._fine_strategy = s
    def add_book(self, book: Book): self._catalog[book.isbn] = book
    def register_member(self, member: Member): self._members[member.id] = member

    def issue_book(self, member_id: str, isbn: str) -> BookLoan:
        with self._lock:
            member = self._members.get(member_id)
            if not member: raise RuntimeError(f"Member not found: {member_id}")
            if not member.can_borrow():
                raise RuntimeError(f"{member.name} reached limit ({member.type.max_books})")

            book = self._catalog.get(isbn)
            if not book: raise RuntimeError(f"Book not found: {isbn}")

            copy = book.get_available_copy()
            if not copy:
                raise RuntimeError(f"No copies available for: {book.title}")

            copy.mark_issued()
            loan = BookLoan(member, copy, book.title)
            self._active_loans[loan.id] = loan
            member.add_loan(loan.id)
            for obs in self._observers: obs.on_book_issued(loan)
            return loan

    def return_book(self, loan_id: str, return_date: Optional[date] = None) -> float:
        with self._lock:
            loan = self._active_loans.get(loan_id)
            if not loan: raise RuntimeError(f"Loan not found: {loan_id}")

            ret_date = return_date or date.today()
            days_over = max(0, (ret_date - loan.due_date).days)
            fine = self._fine_strategy.calculate_fine(days_over)

            loan.mark_returned(fine, ret_date)
            loan.book_copy.mark_returned()
            loan.member.remove_loan(loan_id)
            del self._active_loans[loan_id]
            for obs in self._observers: obs.on_book_returned(loan)

            isbn = loan.book_copy.isbn
            queue = self._reservations.get(isbn)
            if queue:
                res = queue.popleft()
                book = self._catalog[isbn]
                for obs in self._observers:
                    obs.on_reservation_available(res.member, book.title)
                if not queue:
                    del self._reservations[isbn]

            return fine

    def reserve_book(self, member_id: str, isbn: str):
        with self._lock:
            member = self._members.get(member_id)
            book = self._catalog.get(isbn)
            if not member or not book: raise RuntimeError("Not found")
            if book.available_count > 0:
                raise RuntimeError("Book is available - issue directly")
            self._reservations.setdefault(isbn, deque()).append(Reservation(member, isbn))
            print(f"  {member.name} reserved '{book.title}'")

    def search_by_title(self, query: str) -> list[Book]:
        q = query.lower()
        return [b for b in self._catalog.values() if q in b.title.lower()]

    def search_by_author(self, query: str) -> list[Book]:
        q = query.lower()
        return [b for b in self._catalog.values() if q in b.author.lower()]

    def display_catalog(self):
        print("\n--- Library Catalog ---")
        for b in self._catalog.values(): print(f"  {b}")

    def display_members(self):
        print("\n--- Members ---")
        for m in self._members.values(): print(f"  {m}")

# โ”€โ”€โ”€ Main Demo โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def main():
    print("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•")
    print("   LIBRARY MANAGEMENT - LLD DEMO      ")
    print("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n")

    library = Library(TieredFine())
    library.add_observer(LoggingObserver())

    print("--- Adding Books ---")
    library.add_book(Book("978-0-13-468599-1", "Clean Code", "Robert C. Martin", "Programming", 3))
    library.add_book(Book("978-0-201-63361-0", "Design Patterns", "Gang of Four", "Programming", 2))
    library.add_book(Book("978-0-596-51774-8", "JavaScript Good Parts", "Douglas Crockford", "Programming", 1))
    library.add_book(Book("978-0-06-112008-4", "To Kill a Mockingbird", "Harper Lee", "Fiction", 4))
    library.add_book(Book("978-0-14-028329-7", "1984", "George Orwell", "Fiction", 2))
    library.display_catalog()

    print("\n--- Registering Members ---")
    library.register_member(Member("M1", "Rahul", MemberType.STANDARD))
    library.register_member(Member("M2", "Priya", MemberType.PREMIUM))
    library.register_member(Member("M3", "Arjun", MemberType.STANDARD))
    library.display_members()

    print("\n--- Issuing Books ---")
    loan1 = library.issue_book("M1", "978-0-13-468599-1")
    loan2 = library.issue_book("M2", "978-0-13-468599-1")
    loan3 = library.issue_book("M1", "978-0-06-112008-4")
    loan4 = library.issue_book("M3", "978-0-596-51774-8")

    print("\n--- Search ---")
    print("Search 'clean':")
    for b in library.search_by_title("clean"): print(f"  {b}")
    print("Search by author 'orwell':")
    for b in library.search_by_author("orwell"): print(f"  {b}")

    print("\n--- Reservation ---")
    try:
        library.issue_book("M2", "978-0-596-51774-8")
    except RuntimeError as e:
        print(f"  {e}")
    library.reserve_book("M2", "978-0-596-51774-8")

    print("\n--- Returning Books (on time) ---")
    fine1 = library.return_book(loan1.id)
    print(f"  Fine: Rs{fine1}")

    print("\n--- Returning Overdue Book (10 days late) ---")
    overdue_date = loan3.due_date + timedelta(days=10)
    fine3 = library.return_book(loan3.id, overdue_date)
    print(f"  Fine (10 days overdue): Rs{fine3:.2f}")

    print("\n--- Returning JS Book (triggers reservation) ---")
    library.return_book(loan4.id)

    print("\n--- Borrowing Limit Test ---")
    library.issue_book("M1", "978-0-201-63361-0")
    library.issue_book("M1", "978-0-14-028329-7")
    library.issue_book("M1", "978-0-06-112008-4")
    try:
        library.issue_book("M1", "978-0-13-468599-1")
    except RuntimeError as e:
        print(f"  Expected: {e}")

    print("\n--- Fine Strategy Comparison ---")
    print(f"  Per-day (Rs5):  10 days = Rs{PerDayFine(5).calculate_fine(10)}")
    print(f"  Tiered:         10 days = Rs{TieredFine().calculate_fine(10)}")
    print(f"  Flat (Rs50):    10 days = Rs{FlatFine(50).calculate_fine(10)}")

    library.display_catalog()
    library.display_members()

    print("\nโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•")
    print("           DEMO COMPLETE               ")
    print("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•")

if __name__ == "__main__":
    main()
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include <queue>
#include <mutex>
#include <memory>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <random>
#include <stdexcept>

// โ”€โ”€โ”€ Enums โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
enum class MemberType { STANDARD, PREMIUM };
enum class BookStatus { AVAILABLE, ISSUED, RESERVED, LOST };

int maxBooks(MemberType t) { return t == MemberType::STANDARD ? 3 : 10; }
int loanDays(MemberType t) { return t == MemberType::STANDARD ? 14 : 30; }

std::string genLoanId() {
    static std::mt19937 gen(std::random_device{}());
    static std::uniform_int_distribution<int> dist(0, 15);
    static const char* hex = "0123456789abcdef";
    std::string id = "LOAN-";
    for (int i = 0; i < 6; ++i) id += hex[dist(gen)];
    return id;
}

// โ”€โ”€โ”€ Simple Date (days since epoch for simplicity) โ”€โ”€โ”€โ”€โ”€โ”€
struct SimpleDate {
    int days; // days from some reference
    SimpleDate(int d = 0) : days(d) {}
    SimpleDate plusDays(int n) const { return {days + n}; }
    bool isAfter(SimpleDate o) const { return days > o.days; }
    int daysUntil(SimpleDate o) const { return o.days - days; }
    std::string str() const { return "day-" + std::to_string(days); }
};
SimpleDate today() { return {100}; } // simulated "today"

// โ”€โ”€โ”€ Book Copy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
struct BookCopy {
    std::string copyId, isbn;
    BookStatus status = BookStatus::AVAILABLE;
    bool isAvailable() const { return status == BookStatus::AVAILABLE; }
    void markIssued() { status = BookStatus::ISSUED; }
    void markReturned() { status = BookStatus::AVAILABLE; }
};

// โ”€โ”€โ”€ Book โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class Book {
public:
    std::string isbn, title, author, category;
    std::vector<BookCopy> copies;

    Book(std::string isbn, std::string title, std::string author,
         std::string cat, int numCopies)
        : isbn(std::move(isbn)), title(std::move(title)),
          author(std::move(author)), category(std::move(cat)) {
        for (int i = 1; i <= numCopies; ++i) {
            copies.push_back({this->isbn + "-C" + std::to_string(i), this->isbn});
        }
    }

    BookCopy* getAvailableCopy() {
        for (auto& c : copies) if (c.isAvailable()) return &c;
        return nullptr;
    }

    int availableCount() const {
        int cnt = 0;
        for (auto& c : copies) if (c.isAvailable()) cnt++;
        return cnt;
    }

    std::string toString() const {
        return "Book[" + title + " by " + author + " | " +
               std::to_string(availableCount()) + "/" +
               std::to_string(copies.size()) + " available]";
    }
};

// โ”€โ”€โ”€ Member โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class Member {
public:
    std::string id, name;
    MemberType type;
    std::vector<std::string> activeLoanIds;

    Member(std::string id, std::string name, MemberType type)
        : id(std::move(id)), name(std::move(name)), type(type) {}

    bool canBorrow() const { return (int)activeLoanIds.size() < maxBooks(type); }
    void addLoan(const std::string& lid) { activeLoanIds.push_back(lid); }
    void removeLoan(const std::string& lid) {
        activeLoanIds.erase(
            std::remove(activeLoanIds.begin(), activeLoanIds.end(), lid),
            activeLoanIds.end());
    }
};

// โ”€โ”€โ”€ Book Loan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
struct BookLoan {
    std::string id;
    Member* member;
    BookCopy* bookCopy;
    std::string bookTitle;
    SimpleDate issueDate, dueDate, returnDate;
    double fineAmount = 0;
    bool returned = false;

    int daysOverdue(SimpleDate retDate) const {
        if (retDate.isAfter(dueDate)) return retDate.days - dueDate.days;
        return 0;
    }
};

// โ”€โ”€โ”€ Fine Strategy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class FineStrategy {
public:
    virtual ~FineStrategy() = default;
    virtual double calculateFine(int daysOverdue) = 0;
};

class PerDayFine : public FineStrategy {
    double rate;
public:
    PerDayFine(double r) : rate(r) {}
    double calculateFine(int days) override { return days * rate; }
};

class TieredFine : public FineStrategy {
public:
    double calculateFine(int days) override {
        if (days <= 0) return 0;
        if (days <= 7) return days * 2.0;
        if (days <= 14) return 14 + (days - 7) * 5.0;
        return 14 + 35 + (days - 14) * 10.0;
    }
};

class FlatFine : public FineStrategy {
    double amount;
public:
    FlatFine(double a) : amount(a) {}
    double calculateFine(int days) override { return days > 0 ? amount : 0; }
};

// โ”€โ”€โ”€ Reservation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
struct Reservation {
    Member* member;
    std::string isbn;
};

// โ”€โ”€โ”€ Library โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class Library {
    std::unordered_map<std::string, std::unique_ptr<Book>> catalog;
    std::unordered_map<std::string, std::unique_ptr<Member>> members;
    std::unordered_map<std::string, std::unique_ptr<BookLoan>> activeLoans;
    std::unordered_map<std::string, std::queue<Reservation>> reservations;
    std::shared_ptr<FineStrategy> fineStrategy;
    std::mutex mtx;

public:
    Library(std::shared_ptr<FineStrategy> fs) : fineStrategy(std::move(fs)) {}

    void setFineStrategy(std::shared_ptr<FineStrategy> fs) { fineStrategy = std::move(fs); }

    Book* addBook(std::string isbn, std::string title, std::string author,
                  std::string cat, int copies) {
        auto book = std::make_unique<Book>(isbn, title, author, cat, copies);
        Book* ptr = book.get();
        catalog[isbn] = std::move(book);
        return ptr;
    }

    Member* registerMember(std::string id, std::string name, MemberType type) {
        auto m = std::make_unique<Member>(id, name, type);
        Member* ptr = m.get();
        members[id] = std::move(m);
        return ptr;
    }

    BookLoan* issueBook(const std::string& memberId, const std::string& isbn) {
        std::lock_guard<std::mutex> lock(mtx);
        auto mIt = members.find(memberId);
        if (mIt == members.end()) throw std::runtime_error("Member not found");
        Member* member = mIt->second.get();

        if (!member->canBorrow())
            throw std::runtime_error(member->name + " reached borrowing limit");

        auto bIt = catalog.find(isbn);
        if (bIt == catalog.end()) throw std::runtime_error("Book not found");
        Book* book = bIt->second.get();

        BookCopy* copy = book->getAvailableCopy();
        if (!copy) throw std::runtime_error("No copies available for: " + book->title);

        copy->markIssued();
        auto loan = std::make_unique<BookLoan>();
        loan->id = genLoanId();
        loan->member = member;
        loan->bookCopy = copy;
        loan->bookTitle = book->title;
        loan->issueDate = today();
        loan->dueDate = today().plusDays(loanDays(member->type));

        BookLoan* ptr = loan.get();
        member->addLoan(ptr->id);
        activeLoans[ptr->id] = std::move(loan);

        std::cout << "  [ISSUED] " << book->title << " to " << member->name
                  << " (due: " << ptr->dueDate.str() << ")\n";
        return ptr;
    }

    double returnBook(const std::string& loanId, SimpleDate returnDate = today()) {
        std::lock_guard<std::mutex> lock(mtx);
        auto it = activeLoans.find(loanId);
        if (it == activeLoans.end()) throw std::runtime_error("Loan not found");
        BookLoan* loan = it->second.get();

        int overdue = loan->daysOverdue(returnDate);
        double fine = fineStrategy->calculateFine(overdue);
        loan->returnDate = returnDate;
        loan->fineAmount = fine;
        loan->returned = true;
        loan->bookCopy->markReturned();
        loan->member->removeLoan(loanId);

        std::string fineStr = fine > 0 ?
            " | Fine: Rs" + std::to_string((int)fine) : "";
        std::cout << "  [RETURNED] " << loan->bookTitle << " by "
                  << loan->member->name << fineStr << "\n";

        // Check reservations
        auto rIt = reservations.find(loan->bookCopy->isbn);
        if (rIt != reservations.end() && !rIt->second.empty()) {
            Reservation res = rIt->second.front();
            rIt->second.pop();
            std::cout << "  [RESERVED] " << loan->bookTitle
                      << " now available for " << res.member->name << "\n";
        }

        activeLoans.erase(loanId);
        return fine;
    }

    void reserveBook(const std::string& memberId, const std::string& isbn) {
        std::lock_guard<std::mutex> lock(mtx);
        Member* member = members[memberId].get();
        Book* book = catalog[isbn].get();
        if (book->availableCount() > 0)
            throw std::runtime_error("Book available - issue directly");
        reservations[isbn].push({member, isbn});
        std::cout << "  " << member->name << " reserved '" << book->title << "'\n";
    }

    void displayCatalog() {
        std::cout << "\n--- Library Catalog ---\n";
        for (auto& [_, b] : catalog) std::cout << "  " << b->toString() << "\n";
    }
};

// โ”€โ”€โ”€ Main Demo โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
int main() {
    std::cout << "=======================================\n";
    std::cout << "   LIBRARY MANAGEMENT - LLD DEMO      \n";
    std::cout << "=======================================\n\n";

    Library library(std::make_shared<TieredFine>());

    std::cout << "--- Adding Books ---\n";
    library.addBook("ISBN-001", "Clean Code", "Robert Martin", "Programming", 3);
    library.addBook("ISBN-002", "Design Patterns", "Gang of Four", "Programming", 2);
    library.addBook("ISBN-003", "JS Good Parts", "Douglas Crockford", "Programming", 1);
    library.addBook("ISBN-004", "Mockingbird", "Harper Lee", "Fiction", 4);
    library.displayCatalog();

    std::cout << "\n--- Registering Members ---\n";
    library.registerMember("M1", "Rahul", MemberType::STANDARD);
    library.registerMember("M2", "Priya", MemberType::PREMIUM);
    library.registerMember("M3", "Arjun", MemberType::STANDARD);

    std::cout << "\n--- Issuing Books ---\n";
    auto* loan1 = library.issueBook("M1", "ISBN-001");
    auto* loan2 = library.issueBook("M2", "ISBN-001");
    auto* loan3 = library.issueBook("M1", "ISBN-004");
    auto* loan4 = library.issueBook("M3", "ISBN-003");

    std::cout << "\n--- Reservation ---\n";
    try { library.issueBook("M2", "ISBN-003"); }
    catch (const std::runtime_error& e) { std::cout << "  " << e.what() << "\n"; }
    library.reserveBook("M2", "ISBN-003");

    std::cout << "\n--- Returning (on time) ---\n";
    library.returnBook(loan1->id);

    std::cout << "\n--- Returning Overdue (10 days late) ---\n";
    SimpleDate overdueDate = loan3->dueDate.plusDays(10);
    double fine = library.returnBook(loan3->id, overdueDate);
    std::cout << "  Fine (10 days): Rs" << fine << "\n";

    std::cout << "\n--- Return JS Book (triggers reservation) ---\n";
    library.returnBook(loan4->id);

    std::cout << "\n--- Borrowing Limit Test ---\n";
    library.issueBook("M1", "ISBN-002");
    library.issueBook("M1", "ISBN-004");
    library.issueBook("M1", "ISBN-001");
    try { library.issueBook("M1", "ISBN-002"); }
    catch (const std::runtime_error& e) { std::cout << "  Expected: " << e.what() << "\n"; }

    std::cout << "\n--- Fine Comparison ---\n";
    std::cout << "  PerDay(Rs5): 10 days = Rs" << PerDayFine(5).calculateFine(10) << "\n";
    std::cout << "  Tiered:      10 days = Rs" << TieredFine().calculateFine(10) << "\n";
    std::cout << "  Flat(Rs50):  10 days = Rs" << FlatFine(50).calculateFine(10) << "\n";

    library.displayCatalog();

    std::cout << "\n=======================================\n";
    std::cout << "           DEMO COMPLETE               \n";
    std::cout << "=======================================\n";
    return 0;
}

State Transitions

stateDiagram-v2
    [*] --> AVAILABLE
    AVAILABLE --> ISSUED : member borrows
    ISSUED --> AVAILABLE : returned on time
    ISSUED --> OVERDUE : past due date
    OVERDUE --> AVAILABLE : returned with fine
    AVAILABLE --> RESERVED : reservation exists
    RESERVED --> ISSUED : reserved member borrows
    ISSUED --> LOST : reported lost
    LOST --> [*]

Sequence Diagram - Issue and Return

sequenceDiagram
    participant Mem as Member
    participant Lib as Library
    participant Book
    participant Copy as BookCopy
    participant FS as FineStrategy
    participant Obs as Observer

    Mem->>Lib: issueBook(memberId, isbn)
    Lib->>Lib: check member can borrow
    Lib->>Book: getAvailableCopy()
    Book-->>Lib: BookCopy
    Lib->>Copy: markIssued()
    Lib->>Lib: create BookLoan
    Lib->>Obs: onBookIssued(loan)
    Lib-->>Mem: BookLoan

    Note over Mem,Lib: Time passes...

    Mem->>Lib: returnBook(loanId)
    Lib->>Lib: find loan
    Lib->>FS: calculateFine(daysOverdue)
    FS-->>Lib: fine amount
    Lib->>Copy: markReturned()
    Lib->>Lib: check reservations
    Lib->>Obs: onBookReturned(loan)
    Lib-->>Mem: fine amount

How to Extend

Extension Implementation
E-books New DigitalBook subclass with no physical copies; unlimited simultaneous borrows
Inter-library loans Bridge pattern connecting multiple Library instances
Barcode/QR scanning Map barcode โ†’ copyId for quick check-in/check-out
Membership renewal Add expiry date to Member; reject borrows if expired
Book recommendations Track borrow history โ†’ collaborative filtering
SMS/email notifications Multiple observer implementations for different channels
Late return reminders Scheduled task checks overdue loans daily, notifies members

What Interviewers Look For

  1. โœ… Strategy pattern for fine calculation - multiple algorithms swappable
  2. โœ… Reservation queue with FIFO auto-promotion
  3. โœ… Borrowing limits per member type
  4. โœ… Thread-safety on issue/return
  5. โœ… Observer pattern for notifications
  6. โœ… Book copies vs Book entity (one book can have multiple physical copies)
  7. โœ… Overdue tracking with configurable fine computation
  8. โœ… Search by title, author, ISBN


Scale this design past a single process and these are the concepts it runs into:

Discussion

Newest first
You

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access