Case Studies

Library Management System

Design a library system to track books, members, loans, and reservations.

3 min readDesigns · LLD & Patterns

Interview practice: try designing this yourself before reading. Go to the exercise ↓

Problem Statement

Design a library system that:

  • Catalogs books (title, author, ISBN, copies)
  • Registers members
  • Handles checkout / return
  • Manages holds / reservations on unavailable copies
  • Computes late fees
  • Searches catalog by title / author / ISBN

Requirements

Functional

  • Add / remove books and copies
  • Register / suspend members
  • Check out a copy (only if available, member in good standing)
  • Return a copy (compute late fee if overdue)
  • Place a hold; notify when copy is available
  • Search by title, author, ISBN, subject

Non-Functional

  • Concurrent checkouts from multiple branches
  • Audit trail (who borrowed what, when)
  • Persistence (DB-backed)

Domain Model


Core Classes

public class Book {
    private final String isbn;
    private final String title;
    private final List<String> authors;
    private final String subject;

    // constructor / getters
}

public enum CopyStatus { AVAILABLE, CHECKED_OUT, RESERVED, LOST, DAMAGED }

public class BookCopy {
    private final String copyId;
    private final Book book;
    private final Branch branch;
    private CopyStatus status = CopyStatus.AVAILABLE;

    public synchronized boolean checkOut() {
        if (status != CopyStatus.AVAILABLE && status != CopyStatus.RESERVED) return false;
        status = CopyStatus.CHECKED_OUT;
        return true;
    }

    public synchronized void returnCopy() {
        status = CopyStatus.AVAILABLE;
    }
}

public enum MemberStatus { ACTIVE, SUSPENDED, EXPIRED }

public class Member {
    private final String memberId;
    private final String name;
    private MemberStatus status = MemberStatus.ACTIVE;
    private final List<Loan> activeLoans = new ArrayList<>();
    private Money fines = Money.ZERO;

    public boolean canBorrow() {
        return status == MemberStatus.ACTIVE
            && activeLoans.size() < MAX_LOANS
            && fines.compareTo(MAX_FINES) < 0;
    }

    public void addLoan(Loan l) { activeLoans.add(l); }
    public void removeLoan(Loan l) { activeLoans.remove(l); }

    private static final int MAX_LOANS = 5;
    private static final Money MAX_FINES = Money.dollars(20);
}

public class Loan {
    public final BookCopy copy;
    public final Member member;
    public final LocalDate checkoutDate;
    public final LocalDate dueDate;
    public LocalDate returnDate;

    public Loan(BookCopy c, Member m, int loanDays) {
        this.copy = c; this.member = m;
        this.checkoutDate = LocalDate.now();
        this.dueDate = checkoutDate.plusDays(loanDays);
    }
}

public class Catalog {
    private final Map<String, Book> byIsbn = new HashMap<>();
    private final Map<String, Set<Book>> byAuthor = new HashMap<>();
    private final Map<String, Set<Book>> byTitleWord = new HashMap<>();   // inverted index

    public void add(Book b) {
        byIsbn.put(b.getIsbn(), b);
        for (String author : b.getAuthors())
            byAuthor.computeIfAbsent(author, k -> new HashSet<>()).add(b);
        for (String word : tokenize(b.getTitle()))
            byTitleWord.computeIfAbsent(word, k -> new HashSet<>()).add(b);
    }

    public Book findByIsbn(String isbn) { return byIsbn.get(isbn); }

    public Set<Book> findByAuthor(String name) {
        return byAuthor.getOrDefault(name, Set.of());
    }

    public Set<Book> findByTitle(String query) {
        Set<Book> result = null;
        for (String word : tokenize(query)) {
            Set<Book> hits = byTitleWord.getOrDefault(word, Set.of());
            result = (result == null) ? new HashSet<>(hits)
                                      : intersect(result, hits);
        }
        return result == null ? Set.of() : result;
    }

    private List<String> tokenize(String s) {
        return Arrays.stream(s.toLowerCase().split("\\s+")).toList();
    }
}

Library Service (Facade)

public class LibraryService {
    private static final int LOAN_DAYS = 14;
    private static final Money LATE_FEE_PER_DAY = Money.cents(25);

    private final Catalog catalog;
    private final Map<String, BookCopy> copies = new HashMap<>();
    private final HoldQueue holds = new HoldQueue();
    private final NotificationService notifier;

    public Loan checkout(Member m, String copyId) {
        if (!m.canBorrow()) throw new IneligibleMemberException();

        BookCopy copy = copies.get(copyId);
        if (copy == null) throw new UnknownCopyException();
        if (!copy.checkOut()) throw new CopyUnavailableException();

        Loan loan = new Loan(copy, m, LOAN_DAYS);
        m.addLoan(loan);
        return loan;
    }

    public Money returnCopy(Loan loan) {
        loan.returnDate = LocalDate.now();
        loan.copy.returnCopy();
        loan.member.removeLoan(loan);

        Money fee = computeLateFee(loan);
        if (fee.compareTo(Money.ZERO) > 0) {
            loan.member.addFine(fee);
        }

        // Notify next member in hold queue
        Hold next = holds.dequeueFor(loan.copy.getBook());
        if (next != null) {
            loan.copy.reserveFor(next.member);
            notifier.notify(next.member, "Your hold is ready");
        }

        return fee;
    }

    private Money computeLateFee(Loan loan) {
        long daysLate = ChronoUnit.DAYS.between(loan.dueDate, loan.returnDate);
        if (daysLate <= 0) return Money.ZERO;
        return LATE_FEE_PER_DAY.times(daysLate);
    }

    public Hold placeHold(Member m, Book b) {
        Hold h = new Hold(b, m);
        holds.enqueue(h);
        return h;
    }
}

Hold Queue (FIFO per book)

public class HoldQueue {
    private final Map<String, Deque<Hold>> queues = new HashMap<>();   // ISBN -> queue

    public synchronized void enqueue(Hold h) {
        queues.computeIfAbsent(h.book.getIsbn(), k -> new ArrayDeque<>()).addLast(h);
    }

    public synchronized Hold dequeueFor(Book b) {
        Deque<Hold> q = queues.get(b.getIsbn());
        return q == null ? null : q.pollFirst();
    }
}

Sequence: Checkout


Edge Cases

CaseHandling
Member has fines > limitReject checkout
All copies of a book are outAllow placing a hold
Lost copyMark LOST, charge replacement cost to member
Damaged copy on returnMark DAMAGED, charge fee, set aside
Two branches, member borrows from one returns to anotherAllow; copy's branch updated
Hold on book with no copies (yet to acquire)Queue persists until copy added
Member account expires mid-loanLoan still valid; renewal blocked

Design Patterns Used

PatternWhere
FacadeLibraryService simplifies multi-step flows
StateCopyStatus, MemberStatus
StrategyLate-fee calculation (per-day, escalating, capped)
ObserverNotify members when holds become available
RepositoryCatalog, LoanRepository for persistence abstraction
SingletonLibraryService (per app instance)

Interview Tips

  • Distinguish Book (the work) from BookCopy (the physical item) — interviewers test this. Two members can't borrow the same copy, but can borrow different copies of the same book.
  • Catalog search: mention an inverted index for word-level title search.
  • For multi-branch, mention each BookCopy has a branch and you can transfer between branches.
  • Late fees and member status are business rules — pull them into a Strategy if asked about extension.

Practice

Try it yourself

Design a library management system for books, members and loans.

Your design should cover

  • Catalog of titles with multiple physical copies
  • Members borrow and return copies, with limits per member
  • Place holds on a title; the next copy goes to the first in line
  • Fines for late returns
  • Search by title, author or subject
0 / 8,000 characters · saved in this browser

Finished reading?

Mark it done to track your progress in Designs.