Hirestack
Hirestack
Chapter 8

Problem 4: Design a Library Management System

📖 5 min read · 11 sections · May 2026
Asked at: Amazon Microsoft Indian product cos.

The setup

"Design a library management system. Members can borrow and return books, search the catalog, reserve a book that's currently checked out. The library has multiple branches; each branch has its own copies. Librarians can add new books, manage memberships, and process fines for late returns."

This problem tests rich domain modelling: a library is full of nouns (Book, Member, Branch, Loan, Reservation, Fine) and the relationships are non-trivial (one Book has many Copies; a Member can have many Loans; a Copy belongs to one Branch). Interviewers want to see whether you can identify distinct concepts that look similar.

Clarifying questions

QuestionStory answer
Distinction between a "book" and a "copy"?Yes. "Hamlet" is one Book; the library has 5 physical copies of Hamlet. Each copy has its own ID.
Loan duration, fine structure?14-day loan; ₹5/day fine after due date; capped at the cost of the book.
Reservation rules?If a book has no available copies, member can reserve; first-come-first-served when a copy is returned.
Member types — student, faculty, public?Yes — different loan limits and durations.
Search by what?Title, author, ISBN, genre.
Branch ownership of copies?Each copy belongs to one branch. Members can return to any branch (we transfer back).

Core entities and the key insight

The most important distinction:

ConceptDescription
BookThe abstract work. "1984 by Orwell, ISBN 978-..." — one record across the whole library system.
BookCopyA specific physical artifact. 5 copies of "1984" in branch A; 3 in branch B = 8 BookCopy records.
LoanA specific lending event: Member M borrowed Copy C on date D, due date D+14.

This trio captures the essence of inventory management: an abstract item, physical instances of it, and historical events involving instances.

class Book {
  String isbn;
  String title;
  List authors;
  Genre genre;
  // Aggregate: many copies refer to one Book
}

class BookCopy {
  long copyId;
  Book book;
  Branch branch;
  CopyStatus status;  // AVAILABLE, ON_LOAN, RESERVED, LOST, DAMAGED
}

class Loan {
  long loanId;
  Member member;
  BookCopy copy;
  LocalDate borrowedAt;
  LocalDate dueAt;
  LocalDate returnedAt;  // null if still on loan
  BigDecimal fineAccrued;
}

Member hierarchy

Different member types have different rules. Inheritance for type-specific behavior:

abstract class Member {
  long memberId;
  String name;
  String email;
  List activeLoans;
  
  abstract int maxLoans();
  abstract int loanDurationDays();
  abstract BigDecimal finePerDay();
}

class StudentMember extends Member {
  int maxLoans() { return 3; }
  int loanDurationDays() { return 14; }
  BigDecimal finePerDay() { return new BigDecimal("5"); }
}

class FacultyMember extends Member {
  int maxLoans() { return 10; }
  int loanDurationDays() { return 60; }
  BigDecimal finePerDay() { return BigDecimal.ZERO; }  // faculty don't pay fines
}

class PublicMember extends Member { ... }

The methods maxLoans(), loanDurationDays(), finePerDay() are subclass-specific. Strategy pattern would work too (a BorrowingPolicy object held by Member), but inheritance is straightforward here.

Library service / facade

class LibrarySystem {
  private final BookRepository bookRepo;
  private final CopyRepository copyRepo;
  private final LoanRepository loanRepo;
  private final ReservationRepository reservationRepo;
  private final FineCalculator fineCalculator;
  private final NotificationService notifier;
  
  public Loan borrow(Member member, Book book, Branch branch) {
    // Validate member can borrow more
    if (member.activeLoans.size() >= member.maxLoans()) {
      throw new BorrowLimitExceededException();
    }
    // Find an available copy at this branch
    BookCopy copy = copyRepo.findAvailable(book, branch);
    if (copy == null) {
      throw new NoCopyAvailableException("Try reserving");
    }
    // Create loan
    copy.status = CopyStatus.ON_LOAN;
    LocalDate due = LocalDate.now().plusDays(member.loanDurationDays());
    Loan loan = new Loan(member, copy, LocalDate.now(), due);
    loanRepo.save(loan);
    return loan;
  }
  
  public void returnCopy(Loan loan) {
    LocalDate now = LocalDate.now();
    if (now.isAfter(loan.dueAt)) {
      BigDecimal fine = fineCalculator.calculate(loan, now);
      loan.fineAccrued = fine;
      // Charge fine logic
    }
    loan.returnedAt = now;
    loan.copy.status = CopyStatus.AVAILABLE;
    loanRepo.save(loan);
    
    // Check reservations
    Reservation r = reservationRepo.findOldestForBook(loan.copy.book);
    if (r != null) {
      // Notify the reserving member; hold copy for them
      loan.copy.status = CopyStatus.RESERVED;
      notifier.notifyAvailable(r.member, loan.copy);
    }
  }
  
  public Reservation reserve(Member member, Book book) {
    // No available copies; queue the request
    Reservation r = new Reservation(member, book, Instant.now());
    reservationRepo.save(r);
    return r;
  }
}

Design Decision 1: Notification on return

When a copy is returned and there's a reservation, the reserving member must be notified. Use the Observer pattern implicitly: the LibrarySystem notifies via NotificationService.

For more complex notification logic (multiple stakeholders, email + SMS + push), the Observer pattern formally:

interface LibraryEventListener {
  void onCopyReturned(BookCopy copy);
  void onCopyAvailable(BookCopy copy);
  void onLoanOverdue(Loan loan);
}

class LibrarySystem {
  private final List listeners;
  
  public void returnCopy(Loan loan) {
    // ... existing logic ...
    fireEvent(listener -> listener.onCopyReturned(copy));
    if (hasReservation(book)) {
      fireEvent(listener -> listener.onCopyAvailable(copy));
    }
  }
}

Now the EmailNotificationListener, AnalyticsListener, and others register independently. New listeners don't require modifying LibrarySystem.

Members search by various fields. Anti-pattern: a method per field (searchByTitle, searchByAuthor, ...). Better: a query object.

class BookSearchCriteria {
  String title;          // partial match
  String author;
  String isbn;           // exact match
  Genre genre;
  // ... more fields
}

interface BookSearchService {
  List search(BookSearchCriteria criteria);
}

For a small library, a simple in-memory search (filter on each field) works. For a large one, plug in Elasticsearch and the implementation changes — but the API stays the same.

Design Decision 3: Late fees

class FineCalculator {
  public BigDecimal calculate(Loan loan, LocalDate returnDate) {
    if (!returnDate.isAfter(loan.dueAt)) return BigDecimal.ZERO;
    long daysLate = ChronoUnit.DAYS.between(loan.dueAt, returnDate);
    BigDecimal rate = loan.member.finePerDay();
    BigDecimal fine = rate.multiply(BigDecimal.valueOf(daysLate));
    // Cap at the book's replacement cost
    BigDecimal cap = loan.copy.book.replacementCost;
    return fine.min(cap);
  }
}

Pulling fine logic into a separate class keeps it testable and replaceable (e.g., promotional periods with reduced fines).

Cross-examination round 1: concurrent borrow

Interviewer: Two members try to borrow the last copy of a book simultaneously. Race?
Yes. The check-availability-then-borrow sequence has a race window. Mitigations: (1) Database-level: SELECT FOR UPDATE on the copy row when finding available copies. The first borrower locks; the second blocks until the first commits or rolls back. (2) Optimistic locking: each BookCopy has a version field; the borrow update specifies the version; conflict means another borrower won the race. The losing borrower retries (sees no copies available) or queues a reservation.

Cross-examination round 2: rules engine vs. inheritance

Interviewer: You used inheritance for member types. What if rules change frequently — e.g., student loan duration becomes 7 days for seniors, 14 for juniors?
Inheritance becomes inflexible. Refactor to composition: Member has a BorrowingPolicy object that encapsulates the rules. Multiple policy types (StandardPolicy, StudentPolicy, FacultyPolicy, SeniorPolicy). Easy to swap at runtime. The original inheritance design is fine for v1 but not for evolving rules.

Takeaway

Library management is a master class in domain modelling. The key insight is distinguishing Book (abstract) from BookCopy (physical) from Loan (event). Many novices conflate these into one class with confused semantics. Once the entities are right, the relationships and behaviors fall into place.

The deeper lesson: name your nouns carefully. Each distinct concept gets its own class. "Book" and "BookCopy" might seem like over-engineering, but they enable correct modelling of inventory, loans, and reservations. Sloppy entity naming is the most common LLD failure mode.