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.”}}]}