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

Source: https://www.techinterview.org/post/3233463587/low-level-design-library-management-system/
Updated: 2026-07-05 · techinterview.org

## 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 <= loan.due_date:
            return 0.0
        days_overdue = (check_date - loan.due_date).days
        return days_overdue * FINE_PER_DAY


class LoanRepository:
    def __init__(self, db):
        self.db = db

    def get_active_loans(self, member_id: str) -> 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
