Low-Level Design: Library Management System (OOP Interview)

Low-Level Design: Library Management System (OOP Interview)

The Library Management System is a comprehensive LLD interview problem testing your ability to model real-world entities with OOP, handle inventory, membership management, and fine calculation. Commonly asked at Amazon, Microsoft, and other companies for mid-senior engineering roles.

Requirements

Functional Requirements

  • Add/remove books from catalog (with multiple copies)
  • Members can borrow and return books
  • Search books by title, author, ISBN, genre
  • Track due dates and calculate late fines
  • Member management: add, suspend, different membership tiers
  • Notifications for due dates and reservations
  • Reserve books when all copies are checked out

Core Entities and Classes

from enum import Enum
from datetime import date, timedelta
from typing import Optional, List
from dataclasses import dataclass
import uuid

class BookStatus(Enum):
    AVAILABLE   = "AVAILABLE"
    CHECKED_OUT = "CHECKED_OUT"
    RESERVED    = "RESERVED"
    LOST        = "LOST"
    DAMAGED     = "DAMAGED"

class MemberStatus(Enum):
    ACTIVE    = "ACTIVE"
    SUSPENDED = "SUSPENDED"
    EXPIRED   = "EXPIRED"

class MembershipTier(Enum):
    BASIC    = "BASIC"    # 3 books, 14 days
    PREMIUM  = "PREMIUM"  # 10 books, 30 days
    STUDENT  = "STUDENT"  # 5 books, 21 days

@dataclass
class BookItem:
    """Physical copy of a book"""
    barcode: str        # unique per copy
    book_id: str        # references Book
    status: BookStatus = BookStatus.AVAILABLE
    rack_number: str = ""
    checked_out_by: Optional[str] = None   # member_id
    due_date: Optional[date] = None
    checkout_date: Optional[date] = None

@dataclass
class Book:
    """Book metadata (one entry for all copies)"""
    isbn: str
    title: str
    author: str
    genre: str
    publisher: str
    year: int
    items: list = None  # list of BookItem (physical copies)

    def __post_init__(self):
        if self.items is None:
            self.items = []

    def available_copies(self) -> list:
        return [item for item in self.items
                if item.status == BookStatus.AVAILABLE]

    def total_copies(self) -> int:
        return len(self.items)

    def available_count(self) -> int:
        return len(self.available_copies())

@dataclass
class Member:
    member_id: str
    name: str
    email: str
    phone: str
    membership_tier: MembershipTier
    status: MemberStatus = MemberStatus.ACTIVE
    join_date: date = None
    expiry_date: date = None

    def __post_init__(self):
        if self.join_date is None:
            self.join_date = date.today()
        if self.expiry_date is None:
            self.expiry_date = date.today() + timedelta(days=365)

    def max_books(self) -> int:
        limits = {
            MembershipTier.BASIC: 3,
            MembershipTier.PREMIUM: 10,
            MembershipTier.STUDENT: 5,
        }
        return limits[self.membership_tier]

    def loan_period_days(self) -> int:
        periods = {
            MembershipTier.BASIC: 14,
            MembershipTier.PREMIUM: 30,
            MembershipTier.STUDENT: 21,
        }
        return periods[self.membership_tier]

    def is_active(self) -> bool:
        return (self.status == MemberStatus.ACTIVE and
                self.expiry_date >= date.today())

Loan and Fine Management

@dataclass
class BookLoan:
    loan_id: str
    member_id: str
    barcode: str         # BookItem barcode
    book_id: str
    checkout_date: date
    due_date: date
    return_date: Optional[date] = None
    fine_amount: float = 0.0
    is_active: bool = True

FINE_PER_DAY = 0.50  # $0.50 per day overdue

class FineCalculator:
    @staticmethod
    def calculate(loan: BookLoan, return_date: date = None) -> float:
        check_date = return_date or date.today()
        if check_date  list:
        return self.db.fetch(
            "SELECT * FROM loans WHERE member_id = ? AND is_active = 1",
            member_id
        )

    def has_outstanding_fines(self, member_id: str) -> bool:
        total = self.db.fetchval(
            "SELECT COALESCE(SUM(fine_amount), 0) FROM loans WHERE member_id = ? AND fine_amount > 0 AND return_date IS NULL",
            member_id
        )
        return total > 0

Library Class (Facade)

class Library:
    def __init__(self):
        self.catalog: dict[str, Book] = {}           # isbn -> Book
        self.members: dict[str, Member] = {}          # member_id -> Member
        self.loans: dict[str, BookLoan] = {}          # loan_id -> BookLoan
        self.reservations: dict[str, list] = {}       # isbn -> [member_ids]
        self.member_loans: dict[str, list] = {}       # member_id -> [loan_ids]
        self.notification_service = NotificationService()

    def add_book(self, book: Book):
        self.catalog[book.isbn] = book

    def add_book_copy(self, isbn: str, barcode: str, rack: str):
        book = self.catalog.get(isbn)
        if not book:
            raise BookNotFoundError(f"Book {isbn} not found")
        item = BookItem(barcode=barcode, book_id=isbn, rack_number=rack)
        book.items.append(item)

    def checkout_book(self, member_id: str, isbn: str) -> BookLoan:
        """Checkout a book for a member"""
        member = self._get_active_member(member_id)
        book = self._get_book(isbn)

        # Validate member can borrow
        active_loans = self.member_loans.get(member_id, [])
        if len(active_loans) >= member.max_books():
            raise BorrowLimitExceededError(
                f"Member has reached borrow limit ({member.max_books()} books)"
            )

        # Find available copy
        available = book.available_copies()
        if not available:
            raise NoAvailableCopyError(f"No copies of '{book.title}' available")

        # Take first available copy
        book_item = available[0]
        loan_period = member.loan_period_days()
        checkout_date = date.today()
        due_date = checkout_date + timedelta(days=loan_period)

        # Create loan
        loan = BookLoan(
            loan_id=str(uuid.uuid4()),
            member_id=member_id,
            barcode=book_item.barcode,
            book_id=isbn,
            checkout_date=checkout_date,
            due_date=due_date,
        )

        # Update item status
        book_item.status = BookStatus.CHECKED_OUT
        book_item.checked_out_by = member_id
        book_item.due_date = due_date
        book_item.checkout_date = checkout_date

        # Track loan
        self.loans[loan.loan_id] = loan
        if member_id not in self.member_loans:
            self.member_loans[member_id] = []
        self.member_loans[member_id].append(loan.loan_id)

        # Send confirmation
        self.notification_service.send_checkout_confirmation(member, book, due_date)
        return loan

    def return_book(self, barcode: str) -> float:
        """Return a book and calculate fine if overdue"""
        # Find active loan for this barcode
        active_loan = next(
            (loan for loan in self.loans.values()
             if loan.barcode == barcode and loan.is_active),
            None
        )
        if not active_loan:
            raise LoanNotFoundError(f"No active loan for barcode {barcode}")

        # Calculate fine
        return_date = date.today()
        fine = FineCalculator.calculate(active_loan, return_date)
        active_loan.fine_amount = fine
        active_loan.return_date = return_date
        active_loan.is_active = False

        # Update book item status
        book = self.catalog[active_loan.book_id]
        book_item = next(item for item in book.items if item.barcode == barcode)
        book_item.checked_out_by = None
        book_item.due_date = None
        book_item.checkout_date = None

        # Check if anyone has reserved this book
        reserved_members = self.reservations.get(active_loan.book_id, [])
        if reserved_members:
            next_member_id = reserved_members.pop(0)
            book_item.status = BookStatus.RESERVED
            # Notify member their reservation is ready
            next_member = self.members[next_member_id]
            self.notification_service.send_reservation_available(
                next_member, book
            )
        else:
            book_item.status = BookStatus.AVAILABLE

        return fine

    def reserve_book(self, member_id: str, isbn: str):
        """Reserve a book when all copies are checked out"""
        member = self._get_active_member(member_id)
        book = self._get_book(isbn)

        if book.available_count() > 0:
            raise BookAvailableError("Book is available for checkout, reservation not needed")

        if isbn not in self.reservations:
            self.reservations[isbn] = []

        if member_id in self.reservations[isbn]:
            raise AlreadyReservedError("Member already has a reservation for this book")

        self.reservations[isbn].append(member_id)
        position = len(self.reservations[isbn])
        return {'position_in_queue': position}

    def search(self, query: str, by: str = 'title') -> list:
        """Search books by title, author, isbn, or genre"""
        query = query.lower()
        results = []
        for book in self.catalog.values():
            if by == 'title' and query in book.title.lower():
                results.append(book)
            elif by == 'author' and query in book.author.lower():
                results.append(book)
            elif by == 'isbn' and query == book.isbn:
                results.append(book)
            elif by == 'genre' and query == book.genre.lower():
                results.append(book)
        return results

    def _get_active_member(self, member_id: str) -> Member:
        member = self.members.get(member_id)
        if not member:
            raise MemberNotFoundError(f"Member {member_id} not found")
        if not member.is_active():
            raise MemberSuspendedError(f"Member account is {member.status.value}")
        return member

    def _get_book(self, isbn: str) -> Book:
        book = self.catalog.get(isbn)
        if not book:
            raise BookNotFoundError(f"Book ISBN {isbn} not found")
        return book

# Custom exceptions
class BookNotFoundError(Exception): pass
class MemberNotFoundError(Exception): pass
class MemberSuspendedError(Exception): pass
class BorrowLimitExceededError(Exception): pass
class NoAvailableCopyError(Exception): pass
class LoanNotFoundError(Exception): pass
class BookAvailableError(Exception): pass
class AlreadyReservedError(Exception): pass

Notification Service

class NotificationService:
    """Observer pattern: notify members of relevant events"""

    def send_checkout_confirmation(self, member: Member, book: Book, due_date: date):
        self._send_email(member.email, "Book Checked Out",
            f"You've borrowed '{book.title}'. Due: {due_date.strftime('%B %d, %Y')}")

    def send_due_reminder(self, member: Member, book: Book, loan: BookLoan):
        days_left = (loan.due_date - date.today()).days
        self._send_email(member.email, "Book Due Soon",
            f"'{book.title}' is due in {days_left} days on {loan.due_date}")

    def send_overdue_notice(self, member: Member, book: Book, fine: float):
        self._send_email(member.email, "Book Overdue",
            f"'{book.title}' is overdue. Current fine: dollar{fine:.2f}")

    def send_reservation_available(self, member: Member, book: Book):
        self._send_email(member.email, "Reservation Available",
            f"'{book.title}' is now available for pickup. Please collect within 3 days.")

    def _send_email(self, to: str, subject: str, body: str):
        # Integration with email service (SendGrid, SES)
        print(f"Email to {to}: [{subject}] {body}")

Design Patterns Used

  • Facade Pattern: Library class provides simplified interface to Book, Member, Loan subsystems
  • Observer Pattern: NotificationService notified of checkout, return, due date events
  • Strategy Pattern: FineCalculator can be swapped for different fine policies (per-day, flat rate, waived for premium members)
  • Factory Pattern: Can create MemberFactory producing different tier members with appropriate defaults

Interview Tips

  • Draw the entity diagram: Book → BookItem (one-to-many), Member → BookLoan (one-to-many), BookLoan → BookItem (one-to-one)
  • Distinguish Book (metadata, shared) from BookItem (physical copy, status)
  • Fine calculation should be a separate, testable class (Single Responsibility)
  • Reservation queue is FIFO — use a list/deque per book ISBN
  • Member borrowing limits depend on membership tier — use polymorphism or a config map


{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”What is the key entity distinction in a Library Management System?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”The most important distinction is between Book (metadata) and BookItem (physical copy). A Book has one entry with ISBN, title, author, and genre — shared across all copies. A BookItem represents a specific physical copy with its own barcode, location (rack), and status (AVAILABLE, CHECKED_OUT, RESERVED). This is a one-to-many relationship: one Book to many BookItems. This models reality: a library might have 5 copies of ‘Clean Code’ — one Book record with 5 BookItems. Getting this distinction right in the interview demonstrates strong data modeling skills.”}},{“@type”:”Question”,”name”:”How do you implement the reservation queue in a library system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”When all copies of a book are checked out, members can reserve it. Use a FIFO queue (list in Python, LinkedList in Java) per book ISBN: reservations[isbn] = deque([member_id1, member_id2, …]). When a copy is returned: dequeue the first member (reservations[isbn].popleft()), set that BookItem’s status to RESERVED (not AVAILABLE), and notify the member. The member has a configurable window (e.g., 3 days) to pick up the reserved copy. If they don’t collect it, move to next in queue. For persistence: store reservation queue in database with position tracking for ordered dequeue.”}},{“@type”:”Question”,”name”:”How do you calculate late fines in a library system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Fine calculation is a candidate for the Strategy Pattern — different libraries may use different policies (per-day flat rate, escalating rates, waived for premium members). Basic implementation: if return_date > due_date, fine = (return_date – due_date).days * fine_per_day. A separate FineCalculator class handles this logic independently. Important: calculate fine BEFORE updating loan status, and store both the return_date and fine_amount in the loan record for audit trail. For overdue reminders: run a daily job that queries all active loans where due_date is within 3 days or already past, and sends notifications.”}},{“@type”:”Question”,”name”:”Which design patterns apply to a library management system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Core patterns: (1) Facade Pattern — Library class is the single entry point, hiding complexity of Book, Member, Loan, and Notification subsystems. (2) Observer Pattern — NotificationService observes checkout, return, overdue events and sends appropriate notifications. (3) Strategy Pattern — Fine calculation policy is interchangeable (flat per-day, escalating, member-tier-based). (4) Factory Pattern — MemberFactory creates appropriate member objects with tier-specific defaults. (5) Repository Pattern — Separate data access layer (BookRepository, MemberRepository) from business logic. For LLD interviews: drawing the UML class diagram first, then identifying patterns, is the expected workflow.”}},{“@type”:”Question”,”name”:”How would you handle concurrent checkouts of the same book copy?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Race condition: two users simultaneously try to checkout the last available copy. Solutions: (1) Database-level: SELECT FOR UPDATE in a transaction locks the BookItem row; second transaction waits, then finds status=CHECKED_OUT and returns ‘unavailable’. (2) Optimistic locking: add version number to BookItem; update WHERE version=X AND status=AVAILABLE — if 0 rows affected, another transaction won the race, return error. (3) Application-level lock: distributed Redis lock on the book ISBN during checkout (Redis SET NX with TTL). For an LLD interview, the database transaction approach is most robust and usually sufficient to mention.”}}]}

Scroll to Top