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