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

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

The library management system is a popular OOP design question that tests your ability to model real-world entities, manage state transitions, handle search, and apply design patterns. Here is a complete implementation covering all aspects interviewers expect.

Requirements

  • Catalog: add/update/remove books (each book can have multiple physical copies)
  • Search: by title, author, ISBN, or genre
  • Checkout: member borrows a copy; return by due date
  • Reservation: member reserves a copy when all are checked out; notified when available
  • Fine calculation: overdue returns incur a daily fine
  • Members: track active loans and reservations per member

Core Data Models

from enum import Enum
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional
from collections import deque
import uuid

class BookStatus(Enum):
    AVAILABLE   = "available"
    CHECKED_OUT = "checked_out"
    RESERVED    = "reserved"
    LOST        = "lost"

class MemberStatus(Enum):
    ACTIVE    = "active"
    SUSPENDED = "suspended"  # overdue fines unpaid

@dataclass
class Book:
    """Represents the bibliographic record (one entry per title)."""
    isbn:      str
    title:     str
    author:    str
    genre:     str
    year:      int

@dataclass
class BookCopy:
    """Represents a physical copy of a book."""
    copy_id:   str = field(default_factory=lambda: str(uuid.uuid4()))
    isbn:      str = ""
    status:    BookStatus = BookStatus.AVAILABLE
    due_date:  Optional[datetime] = None

    def is_available(self) -> bool:
        return self.status == BookStatus.AVAILABLE

@dataclass
class Member:
    member_id:     str
    name:          str
    email:         str
    status:        MemberStatus = MemberStatus.ACTIVE
    active_loans:  list[str] = field(default_factory=list)   # copy_ids
    reservations:  list[str] = field(default_factory=list)   # isbns
    total_fine:    float = 0.0

    def can_borrow(self, max_loans: int = 5) -> bool:
        return (self.status == MemberStatus.ACTIVE
                and len(self.active_loans)  bool:
        check_dt = self.return_dt or datetime.now()
        return check_dt > self.due_dt

    def fine_amount(self, rate_per_day: float = 0.25) -> float:
        if not self.is_overdue():
            return 0.0
        check_dt = self.return_dt or datetime.now()
        days_overdue = (check_dt - self.due_dt).days
        return days_overdue * rate_per_day

Search: Strategy Pattern

from abc import ABC, abstractmethod

class SearchStrategy(ABC):
    @abstractmethod
    def matches(self, book: Book, query: str) -> bool:
        pass

class TitleSearch(SearchStrategy):
    def matches(self, book: Book, query: str) -> bool:
        return query.lower() in book.title.lower()

class AuthorSearch(SearchStrategy):
    def matches(self, book: Book, query: str) -> bool:
        return query.lower() in book.author.lower()

class ISBNSearch(SearchStrategy):
    def matches(self, book: Book, query: str) -> bool:
        return book.isbn == query

class GenreSearch(SearchStrategy):
    def matches(self, book: Book, query: str) -> bool:
        return query.lower() == book.genre.lower()

class Catalog:
    def __init__(self):
        self._books: dict[str, Book] = {}         # isbn → Book
        self._copies: dict[str, list[BookCopy]] = {}  # isbn → [BookCopy]

    def add_book(self, book: Book) -> None:
        self._books[book.isbn] = book
        if book.isbn not in self._copies:
            self._copies[book.isbn] = []

    def add_copy(self, isbn: str) -> BookCopy:
        if isbn not in self._books:
            raise ValueError(f"Book {isbn} not in catalog")
        copy = BookCopy(isbn=isbn)
        self._copies[isbn].append(copy)
        return copy

    def search(self, query: str, strategy: SearchStrategy) -> list[Book]:
        return [book for book in self._books.values() if strategy.matches(book, query)]

    def get_available_copy(self, isbn: str) -> Optional[BookCopy]:
        for copy in self._copies.get(isbn, []):
            if copy.is_available():
                return copy
        return None

    def get_copy(self, copy_id: str) -> Optional[BookCopy]:
        for copies in self._copies.values():
            for copy in copies:
                if copy.copy_id == copy_id:
                    return copy
        return None

Reservation Queue (Observer / Queue Pattern)

class NotificationService:
    def notify(self, member: Member, message: str) -> None:
        print(f"[NOTIFY] {member.email}: {message}")

class ReservationQueue:
    """FIFO queue per ISBN. When a copy is returned, notify the next in queue."""

    def __init__(self, notification_service: NotificationService):
        self._queues: dict[str, deque[str]] = {}  # isbn → deque of member_ids
        self._notifier = notification_service

    def reserve(self, isbn: str, member_id: str) -> None:
        if isbn not in self._queues:
            self._queues[isbn] = deque()
        if member_id not in self._queues[isbn]:
            self._queues[isbn].append(member_id)

    def cancel(self, isbn: str, member_id: str) -> None:
        if isbn in self._queues:
            try:
                self._queues[isbn].remove(member_id)
            except ValueError:
                pass

    def notify_next(self, isbn: str, members: dict[str, Member]) -> Optional[str]:
        """Returns next member_id if anyone is waiting, else None."""
        if isbn not in self._queues or not self._queues[isbn]:
            return None
        member_id = self._queues[isbn].popleft()
        member = members.get(member_id)
        if member:
            self._notifier.notify(member, f"A copy of ISBN {isbn} is now available for pickup.")
        return member_id

Library (Main Orchestrator)

class Library:
    LOAN_DAYS          = 14
    MAX_LOANS_PER_USER = 5
    FINE_PER_DAY       = 0.25

    def __init__(self):
        self.catalog      = Catalog()
        self._members: dict[str, Member] = {}
        self._loans:   dict[str, Loan]   = {}  # loan_id → Loan
        self._copy_loans: dict[str, str] = {}  # copy_id → loan_id
        self._reservations = ReservationQueue(NotificationService())

    # ── Member Management ─────────────────────────────────────────────────────
    def register_member(self, name: str, email: str) -> Member:
        member = Member(member_id=str(uuid.uuid4()), name=name, email=email)
        self._members[member.member_id] = member
        return member

    # ── Checkout ──────────────────────────────────────────────────────────────
    def checkout(self, member_id: str, isbn: str) -> Optional[Loan]:
        member = self._members.get(member_id)
        if not member or not member.can_borrow(self.MAX_LOANS_PER_USER):
            print(f"Cannot borrow: member ineligible or at loan limit")
            return None

        copy = self.catalog.get_available_copy(isbn)
        if not copy:
            print(f"No available copy for ISBN {isbn}. Add to reservation queue?")
            return None

        copy.status   = BookStatus.CHECKED_OUT
        copy.due_date = datetime.now() + timedelta(days=self.LOAN_DAYS)

        loan = Loan(copy_id=copy.copy_id, member_id=member_id)
        loan.due_dt = copy.due_date
        self._loans[loan.loan_id] = loan
        self._copy_loans[copy.copy_id] = loan.loan_id
        member.active_loans.append(copy.copy_id)

        print(f"Checked out {isbn} to {member.name}. Due: {loan.due_dt.date()}")
        return loan

    # ── Return ────────────────────────────────────────────────────────────────
    def return_book(self, copy_id: str) -> float:
        loan_id = self._copy_loans.get(copy_id)
        if not loan_id:
            raise ValueError(f"No active loan for copy {copy_id}")

        loan = self._loans[loan_id]
        loan.return_dt = datetime.now()

        copy = self.catalog.get_copy(copy_id)
        copy.status   = BookStatus.AVAILABLE
        copy.due_date = None

        member = self._members[loan.member_id]
        member.active_loans.remove(copy_id)

        fine = loan.fine_amount(self.FINE_PER_DAY)
        if fine > 0:
            member.total_fine += fine
            if member.total_fine > 10.0:
                member.status = MemberStatus.SUSPENDED
            print(f"Overdue fine: $" + f"{fine:.2f} for {member.name}")

        # Notify next reservation holder
        self._reservations.notify_next(copy.isbn, self._members)

        return fine

    # ── Reservation ───────────────────────────────────────────────────────────
    def reserve(self, member_id: str, isbn: str) -> None:
        self._reservations.reserve(isbn, member_id)
        self._members[member_id].reservations.append(isbn)
        print(f"Reserved ISBN {isbn} for member {member_id}")

    # ── Search ────────────────────────────────────────────────────────────────
    def search_by_title(self, query: str)  -> list[Book]: return self.catalog.search(query, TitleSearch())
    def search_by_author(self, query: str) -> list[Book]: return self.catalog.search(query, AuthorSearch())
    def search_by_isbn(self, isbn: str)    -> list[Book]: return self.catalog.search(isbn, ISBNSearch())

Usage Example

lib = Library()

# Setup catalog
lib.catalog.add_book(Book("978-0-13-110362-7", "The C Programming Language", "Kernighan & Ritchie", "Programming", 1978))
lib.catalog.add_copy("978-0-13-110362-7")
lib.catalog.add_copy("978-0-13-110362-7")  # 2 physical copies

# Register members
alice = lib.register_member("Alice", "alice@example.com")
bob   = lib.register_member("Bob",   "bob@example.com")

# Alice checks out a copy
loan = lib.checkout(alice.member_id, "978-0-13-110362-7")

# Bob reserves (only 1 copy left, let's imagine it's also out)
lib.reserve(bob.member_id, "978-0-13-110362-7")

# Alice returns → Bob gets notified
fine = lib.return_book(loan.copy_id)
# → [NOTIFY] bob@example.com: A copy of ISBN 978-0-13-110362-7 is now available

results = lib.search_by_author("Kernighan")
print([b.title for b in results])

Design Patterns Applied

Pattern Where Benefit
Strategy SearchStrategy (Title/Author/ISBN/Genre) Add new search types without modifying Catalog
Observer ReservationQueue + NotificationService Decouple return event from reservation notification
Factory register_member(), add_copy() Centralize object creation with validation
State Machine BookStatus (AVAILABLE → CHECKED_OUT → AVAILABLE) Explicit valid state transitions; reject invalid ones

Interview Follow-ups

  • Thread safety: Add per-ISBN locks when multiple threads call checkout() concurrently — same pattern as Parking Lot
  • Fine payment: Add a pay_fine(member_id, amount) method; clear suspension when fine reaches 0
  • Renewal: Add renew_loan(loan_id) that extends due_dt by LOAN_DAYS if no one has reserved
  • Persistence: Replace in-memory dicts with database tables; loans and members become SQL rows

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you design a library management system in an OOP interview?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Start with four core entities: Book (ISBN, title, author — bibliographic record), BookCopy (physical copy with status: AVAILABLE, CHECKED_OUT, RESERVED, LOST), Member (loan history, fine balance, status), and Loan (ties member to copy with checkout/due/return dates). The Library class orchestrates: checkout() finds an available copy, creates a Loan, updates statuses; return_book() calculates fines (ceil days overdue * daily rate), vacates the copy, and triggers reservation notifications. Apply Strategy pattern for fee calculation (swappable hourly/daily/flat rates) and Observer pattern for reservation notifications (decouples return event from notification logic).”}},{“@type”:”Question”,”name”:”How do you implement a reservation queue for library books?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use a FIFO deque per ISBN (not per copy, since any copy can satisfy the reservation). When a member reserves ISBN X and all copies are checked out, append their member_id to the ISBN’s queue. When any copy of ISBN X is returned, pop the first member_id from the queue and send them a notification (email/push). The key design decision: notify vs. auto-checkout. Auto-checkout reserves the copy for the notified member for a hold period (e.g., 24 hours); if they don’t pick it up, the copy is released and the next person in queue is notified. This prevents the system from indefinitely reserving books for inactive members.”}},{“@type”:”Question”,”name”:”How do you calculate and enforce overdue fines in a library system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Fine calculation uses the Loan record: fine = ceil((return_dt – due_dt).days) * rate_per_day. Use math.ceil to charge a full day even for partial days past due. Store the running total in Member.total_fine. Enforcement: can_borrow() returns False if total_fine > 0 — member cannot borrow until fines are paid. For suspension: automatically set Member.status = SUSPENDED when fine exceeds a threshold (e.g., $10). Pay_fine() reduces the balance; if it reaches 0, reactivate the member. Lazy vs eager fine assessment: lazy calculates only on return (simpler); eager runs a nightly job to notify members of accruing fines (better UX).”}}]}

🏢 Asked at: Atlassian Interview Guide

🏢 Asked at: Shopify Interview Guide

🏢 Asked at: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering

🏢 Asked at: Apple Interview Guide 2026: iOS Systems, Hardware-Software Integration, and iCloud Architecture

🏢 Asked at: LinkedIn Interview Guide 2026: Social Graph Engineering, Feed Ranking, and Professional Network Scale

🏢 Asked at: Databricks Interview Guide 2026: Spark Internals, Delta Lake, and Lakehouse Architecture

Asked at: Airbnb Interview Guide

Asked at: DoorDash Interview Guide

Asked at: Stripe Interview Guide

Asked at: Cloudflare Interview Guide

Scroll to Top