Design Library Management System

intermediate 2-4 YOE lld library-management design-question

Library Management is the classic CRUD-meets-business-rules LLD question. It sounds simple at first — add books, search, borrow, return. But then the interviewer asks about multiple copies, fines, reservations, and suddenly there’s real design thinking needed.

The key insight that makes or breaks this design: a Book is not the same as a BookItem. “Clean Code” is a Book. The three physical copies on the shelf are BookItems. Get this right and everything else falls into place.

Requirements

Functional:

  • Add, remove, and search books (by title, author, or ISBN)
  • Each book can have multiple physical copies (BookItems)
  • Members can borrow and return books
  • Maximum 5 books per member at a time
  • Loan period is 14 days. Late returns incur a fine ($1/day)
  • Members can reserve a book that’s currently loaned out
  • Notify members when their reserved book becomes available

Constraints:

  • Members must have an active account to borrow
  • A BookItem can only be loaned to one member at a time
  • Librarians can add/remove books and manage members

Key Classes & Relationships

Library Management — Class Diagram
Library
- books: Map<ISBN, Book>
- members: Map<id, Member>
+ searchByTitle(title)
+ borrowBook(memberId, isbn)
+ returnBook(memberId, itemId)
│ manages
Book
- isbn: string
- title: string
- author: string
- items: BookItem[]
Member
- id: string
- name: string
- activeLoans: Loan[]
- totalFines: float
Book has many ↓       Member has many ↓
BookItem
- barcode: string
- status: BookStatus
- isbn: string (→ Book)
Loan
- item: BookItem
- member: Member
- borrowDate: Date
- dueDate: Date
- returnDate: Date?

The Book vs BookItem distinction is the most important modeling decision. A Book is the catalog entry (metadata). A BookItem is a physical copy that can be loaned, returned, lost. One Book can have 5 BookItems, each with its own status.

Core Implementation

from enum import Enum
from datetime import datetime, timedelta

class BookStatus(Enum):
    AVAILABLE = "available"
    LOANED = "loaned"
    RESERVED = "reserved"
    LOST = "lost"

# --- Core Models ---

class BookItem:
    def __init__(self, barcode: str, isbn: str):
        self.barcode = barcode
        self.isbn = isbn
        self.status = BookStatus.AVAILABLE

class Book:
    def __init__(self, isbn: str, title: str, author: str):
        self.isbn = isbn
        self.title = title
        self.author = author
        self.items: list[BookItem] = []

    def add_item(self, barcode: str) -> BookItem:
        item = BookItem(barcode, self.isbn)
        self.items.append(item)
        return item

    def get_available_item(self) -> BookItem | None:
        for item in self.items:
            if item.status == BookStatus.AVAILABLE:
                return item
        return None

class Loan:
    LOAN_DAYS = 14
    FINE_PER_DAY = 1.0

    def __init__(self, item: BookItem, member_id: str):
        self.item = item
        self.member_id = member_id
        self.borrow_date = datetime.now()
        self.due_date = self.borrow_date + timedelta(days=self.LOAN_DAYS)
        self.return_date: datetime | None = None

    def calculate_fine(self) -> float:
        end = self.return_date or datetime.now()
        if end <= self.due_date:
            return 0.0
        overdue_days = (end - self.due_date).days
        return overdue_days * self.FINE_PER_DAY

class Member:
    MAX_LOANS = 5

    def __init__(self, member_id: str, name: str):
        self.member_id = member_id
        self.name = name
        self.active_loans: list[Loan] = []
        self.total_fines = 0.0

    def can_borrow(self) -> bool:
        return len(self.active_loans) < self.MAX_LOANS

# --- Library (Facade) ---

class Library:
    def __init__(self):
        self.books: dict[str, Book] = {}       # isbn → Book
        self.members: dict[str, Member] = {}   # id → Member
        self.observers: dict[str, list[str]] = {}  # isbn → [member_ids waiting]

    def add_book(self, isbn: str, title: str, author: str) -> Book:
        if isbn not in self.books:
            self.books[isbn] = Book(isbn, title, author)
        return self.books[isbn]

    def add_member(self, member_id: str, name: str) -> Member:
        member = Member(member_id, name)
        self.members[member_id] = member
        return member

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

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

    def borrow_book(self, member_id: str, isbn: str) -> Loan | None:
        member = self.members.get(member_id)
        book = self.books.get(isbn)

        if not member or not book:
            print("Member or book not found.")
            return None
        if not member.can_borrow():
            print(f"{member.name} has reached max loans ({Member.MAX_LOANS}).")
            return None

        item = book.get_available_item()
        if not item:
            print(f"No copies available for '{book.title}'.")
            return None

        item.status = BookStatus.LOANED
        loan = Loan(item, member_id)
        member.active_loans.append(loan)
        print(f"{member.name} borrowed '{book.title}' (due: {loan.due_date.date()})")
        return loan

    def return_book(self, member_id: str, barcode: str) -> float:
        member = self.members.get(member_id)
        if not member:
            print("Member not found.")
            return 0.0

        loan = next((l for l in member.active_loans if l.item.barcode == barcode), None)
        if not loan:
            print("No active loan found for this item.")
            return 0.0

        loan.return_date = datetime.now()
        fine = loan.calculate_fine()
        member.total_fines += fine
        loan.item.status = BookStatus.AVAILABLE
        member.active_loans.remove(loan)

        isbn = loan.item.isbn
        print(f"Returned '{self.books[isbn].title}'. Fine: ${fine:.2f}")

        # Notify waiting members
        self._notify_reservation(isbn)
        return fine

    def reserve_book(self, member_id: str, isbn: str):
        if isbn not in self.observers:
            self.observers[isbn] = []
        self.observers[isbn].append(member_id)
        print(f"Member {member_id} added to waitlist for ISBN {isbn}")

    def _notify_reservation(self, isbn: str):
        if isbn in self.observers and self.observers[isbn]:
            book = self.books[isbn]
            if book.get_available_item():
                next_member_id = self.observers[isbn].pop(0)
                member = self.members.get(next_member_id)
                if member:
                    print(f"  Notification: {member.name}, '{book.title}' is now available!")

# Usage
lib = Library()

book = lib.add_book("978-0132350884", "Clean Code", "Robert C. Martin")
book.add_item("CC-001")
book.add_item("CC-002")

alice = lib.add_member("M001", "Alice")
bob = lib.add_member("M002", "Bob")

lib.borrow_book("M001", "978-0132350884")  # Alice borrows copy 1
lib.borrow_book("M002", "978-0132350884")  # Bob borrows copy 2
lib.return_book("M001", "CC-001")           # Alice returns
const BookStatus = Object.freeze({
  AVAILABLE: "available",
  LOANED: "loaned",
  RESERVED: "reserved",
  LOST: "lost",
});

class BookItem {
  constructor(barcode, isbn) {
    this.barcode = barcode;
    this.isbn = isbn;
    this.status = BookStatus.AVAILABLE;
  }
}

class Book {
  constructor(isbn, title, author) {
    this.isbn = isbn;
    this.title = title;
    this.author = author;
    this.items = [];
  }
  addItem(barcode) {
    const item = new BookItem(barcode, this.isbn);
    this.items.push(item);
    return item;
  }
  getAvailableItem() {
    return this.items.find((i) => i.status === BookStatus.AVAILABLE) || null;
  }
}

class Loan {
  static LOAN_DAYS = 14;
  static FINE_PER_DAY = 1.0;

  constructor(item, memberId) {
    this.item = item;
    this.memberId = memberId;
    this.borrowDate = new Date();
    this.dueDate = new Date(Date.now() + Loan.LOAN_DAYS * 86400000);
    this.returnDate = null;
  }

  calculateFine() {
    const end = this.returnDate || new Date();
    if (end <= this.dueDate) return 0;
    const overdueDays = Math.ceil((end - this.dueDate) / 86400000);
    return overdueDays * Loan.FINE_PER_DAY;
  }
}

class Member {
  static MAX_LOANS = 5;
  constructor(id, name) {
    this.id = id;
    this.name = name;
    this.activeLoans = [];
    this.totalFines = 0;
  }
  canBorrow() { return this.activeLoans.length < Member.MAX_LOANS; }
}

class Library {
  constructor() {
    this.books = new Map();    // isbn → Book
    this.members = new Map();  // id → Member
    this.waitlists = new Map(); // isbn → [memberIds]
  }

  addBook(isbn, title, author) {
    if (!this.books.has(isbn)) this.books.set(isbn, new Book(isbn, title, author));
    return this.books.get(isbn);
  }

  addMember(id, name) {
    const m = new Member(id, name);
    this.members.set(id, m);
    return m;
  }

  searchByTitle(query) {
    const q = query.toLowerCase();
    return [...this.books.values()].filter((b) => b.title.toLowerCase().includes(q));
  }

  borrowBook(memberId, isbn) {
    const member = this.members.get(memberId);
    const book = this.books.get(isbn);
    if (!member || !book) return null;
    if (!member.canBorrow()) { console.log("Max loans reached."); return null; }

    const item = book.getAvailableItem();
    if (!item) { console.log(`No copies of '${book.title}' available.`); return null; }

    item.status = BookStatus.LOANED;
    const loan = new Loan(item, memberId);
    member.activeLoans.push(loan);
    console.log(`${member.name} borrowed '${book.title}'`);
    return loan;
  }

  returnBook(memberId, barcode) {
    const member = this.members.get(memberId);
    if (!member) return 0;
    const idx = member.activeLoans.findIndex((l) => l.item.barcode === barcode);
    if (idx === -1) return 0;

    const loan = member.activeLoans.splice(idx, 1)[0];
    loan.returnDate = new Date();
    const fine = loan.calculateFine();
    member.totalFines += fine;
    loan.item.status = BookStatus.AVAILABLE;

    const book = this.books.get(loan.item.isbn);
    console.log(`Returned '${book.title}'. Fine: $${fine.toFixed(2)}`);
    this._notifyWaitlist(loan.item.isbn);
    return fine;
  }

  _notifyWaitlist(isbn) {
    const list = this.waitlists.get(isbn);
    if (!list || list.length === 0) return;
    const book = this.books.get(isbn);
    if (book.getAvailableItem()) {
      const nextId = list.shift();
      const m = this.members.get(nextId);
      if (m) console.log(`  Notification: ${m.name}, '${book.title}' is available!`);
    }
  }
}

// Usage
const lib = new Library();
const book = lib.addBook("978-0132350884", "Clean Code", "Robert C. Martin");
book.addItem("CC-001");
lib.addMember("M001", "Alice");
lib.borrowBook("M001", "978-0132350884");
lib.returnBook("M001", "CC-001");
import java.util.*;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

enum BookStatus { AVAILABLE, LOANED, RESERVED, LOST }

class BookItem {
    String barcode, isbn;
    BookStatus status = BookStatus.AVAILABLE;
    BookItem(String barcode, String isbn) { this.barcode = barcode; this.isbn = isbn; }
}

class Book {
    String isbn, title, author;
    List<BookItem> items = new ArrayList<>();

    Book(String isbn, String title, String author) {
        this.isbn = isbn; this.title = title; this.author = author;
    }
    BookItem addItem(String barcode) {
        BookItem item = new BookItem(barcode, isbn);
        items.add(item);
        return item;
    }
    BookItem getAvailableItem() {
        return items.stream().filter(i -> i.status == BookStatus.AVAILABLE)
                    .findFirst().orElse(null);
    }
}

class Loan {
    static final int LOAN_DAYS = 14;
    static final double FINE_PER_DAY = 1.0;

    BookItem item;
    String memberId;
    LocalDate borrowDate, dueDate;
    LocalDate returnDate;

    Loan(BookItem item, String memberId) {
        this.item = item;
        this.memberId = memberId;
        this.borrowDate = LocalDate.now();
        this.dueDate = borrowDate.plusDays(LOAN_DAYS);
    }

    double calculateFine() {
        LocalDate end = returnDate != null ? returnDate : LocalDate.now();
        if (!end.isAfter(dueDate)) return 0;
        long overdue = ChronoUnit.DAYS.between(dueDate, end);
        return overdue * FINE_PER_DAY;
    }
}

class Member {
    static final int MAX_LOANS = 5;
    String id, name;
    List<Loan> activeLoans = new ArrayList<>();
    double totalFines = 0;

    Member(String id, String name) { this.id = id; this.name = name; }
    boolean canBorrow() { return activeLoans.size() < MAX_LOANS; }
}

class Library {
    Map<String, Book> books = new HashMap<>();
    Map<String, Member> members = new HashMap<>();
    Map<String, Queue<String>> waitlists = new HashMap<>();

    Book addBook(String isbn, String title, String author) {
        books.putIfAbsent(isbn, new Book(isbn, title, author));
        return books.get(isbn);
    }

    Member addMember(String id, String name) {
        Member m = new Member(id, name);
        members.put(id, m);
        return m;
    }

    Loan borrowBook(String memberId, String isbn) {
        Member member = members.get(memberId);
        Book book = books.get(isbn);
        if (member == null || book == null) return null;
        if (!member.canBorrow()) return null;

        BookItem item = book.getAvailableItem();
        if (item == null) {
            System.out.println("No copies available.");
            return null;
        }
        item.status = BookStatus.LOANED;
        Loan loan = new Loan(item, memberId);
        member.activeLoans.add(loan);
        System.out.println(member.name + " borrowed '" + book.title + "'");
        return loan;
    }

    double returnBook(String memberId, String barcode) {
        Member member = members.get(memberId);
        if (member == null) return 0;

        Loan loan = member.activeLoans.stream()
            .filter(l -> l.item.barcode.equals(barcode))
            .findFirst().orElse(null);
        if (loan == null) return 0;

        loan.returnDate = LocalDate.now();
        double fine = loan.calculateFine();
        member.totalFines += fine;
        loan.item.status = BookStatus.AVAILABLE;
        member.activeLoans.remove(loan);
        System.out.println("Returned. Fine: $" + fine);
        return fine;
    }
}

Design Patterns Used

Facade — The Library class is a facade over the entire system. Members don’t interact with BookItem or Loan directly. They call library.borrowBook() and the library handles all the internal coordination — finding available copies, creating loans, updating statuses.

Observer — The reservation/waitlist system. When a book is returned, the library checks if anyone is waiting and notifies them. We store a list of member IDs per ISBN. When a copy becomes available, we pop the first waiter and notify them.

Composition — Book HAS BookItems. Member HAS Loans. Library HAS Books and Members. Everything is connected through references, not inheritance.

Value Objects — Loan captures a point-in-time transaction. It has a borrow date, due date, and optional return date. The fine calculation is derived from these dates. Clean and testable.

Extensions

“Add a reservation system” — When no copies are available, a member can reserve. We already have the waitlist. Extend it so the next available copy gets status RESERVED and is held for 24 hours.

“Support renewal” — Add a renewLoan(memberId, barcode) method. Extend the due date by another 14 days. But only if: no one has reserved it, and the member hasn’t already renewed once.

“Different member tiers” — Gold members get 10 books, Silver gets 7, Basic gets 5. Use a MemberTier enum and look up the limit. Or use Strategy pattern where each tier defines its own borrowing rules.

“Support digital books (e-books)” — This is where we might introduce an interface like Borrowable that both BookItem and DigitalBook implement. Digital books don’t have a barcode or physical status, but they still have loan limits.

“Search with multiple filters” — Add a SearchCriteria builder: search().byTitle("clean").byAuthor("martin").available().execute(). This is a nice spot for the Builder pattern.

In simple language, the Library question is really about modeling the right entities. The moment we separate Book (catalog entry) from BookItem (physical copy), everything clicks. One Book, many copies. Each copy has its own lifecycle. The Library facade ties it all together so nobody has to juggle the internals.