Difficulty: 🟢 Beginner | Time: 35-45 minutes | Patterns: Singleton, Observer, Factory
🎯 Problem Statement
Design a library management system that can:- Manage books, members, and librarians
- Handle book borrowing and returns
- Track due dates and calculate fines
- Support book reservations
- Search books by title, author, or ISBN
Why This Problem? Library is a great beginner problem that covers core OOP concepts - classes, relationships, and basic CRUD operations. Perfect for warming up before harder problems!
📋 Step 1: Clarify Requirements
Interview Tip: Library problems can be simple or complex. Clarify if you need multiple copies, reservations, and notifications.
Questions to Ask the Interviewer
| Category | Question | Impact on Design |
|---|---|---|
| Books | Multiple copies of same book? | BookCopy vs Book |
| Members | Different member types? | Member hierarchy |
| Limits | Max books per member? Loan period? | Validation logic |
| Reservations | Can reserve borrowed books? | Reservation queue |
| Fines | How are fines calculated? | Fine calculator |
| Notifications | Due date reminders? | Observer pattern |
Functional Requirements
- Add/remove books from the library
- Register new members
- Borrow and return books
- Reserve books that are currently borrowed
- Search for books
- Calculate and collect fines for overdue books
- Send notifications for due dates
Non-Functional Requirements
- Support multiple copies of the same book
- Handle concurrent borrowing requests
- Maintain borrowing history
🧩 Step 2: Identify Core Objects
Key Insight: Distinguish between
Book (metadata - title, author, ISBN) and BookCopy (physical copy - barcode, status). One book can have many copies!Books
Book, BookCopy, Rack Location
Users
Member, Librarian, Account
Transactions
Lending, Reservation, Fine
Entity-Responsibility Mapping
| Entity | Responsibilities | Pattern |
|---|---|---|
Library | Manage catalog, coordinate operations | Singleton |
Book | Store metadata, track copies | - |
BookCopy | Track status, location | - |
Member | Borrow, return, pay fines | - |
Lending | Track borrow date, due date | - |
NotificationService | Due date reminders | Observer |
Key Constraints
Copy
MAX_BOOKS_PER_MEMBER = 5
LOAN_PERIOD_DAYS = 14
FINE_PER_DAY = 0.50
MAX_RESERVATION_DAYS = 7
📐 Step 3: Class Diagram
Copy
┌─────────────────────────────────────────────────────────────┐
│ Library │
├─────────────────────────────────────────────────────────────┤
│ - name: String │
│ - books: Dict[ISBN, Book] │
│ - members: Dict[MemberId, Member] │
├─────────────────────────────────────────────────────────────┤
│ + addBook(book): bool │
│ + removeBook(isbn): bool │
│ + registerMember(member): bool │
│ + searchByTitle(title): List<Book> │
│ + searchByAuthor(author): List<Book> │
└─────────────────────────────────────────────────────────────┘
│
┌─────────┴─────────┐
│ │
▼ ▼
┌───────────────────────────┐ ┌───────────────────────────┐
│ Book │ │ Member │
├───────────────────────────┤ ├───────────────────────────┤
│ - isbn: String │ │ - id: String │
│ - title: String │ │ - name: String │
│ - author: String │ │ - email: String │
│ - copies: List<BookCopy> │ │ - borrowedBooks: List │
├───────────────────────────┤ │ - reservations: List │
│ + getAvailableCopy() │ ├───────────────────────────┤
│ + addCopy(copy) │ │ + borrow(book): Lending │
└───────────────────────────┘ │ + return(lending): Fine? │
│ │ + reserve(book): bool │
│ 1..* └───────────────────────────┘
▼
┌───────────────────────────┐
│ BookCopy │
├───────────────────────────┤
│ - barcode: String │
│ - status: CopyStatus │
│ - rack: RackLocation │
├───────────────────────────┤
│ + checkout(): bool │
│ + checkin(): bool │
└───────────────────────────┘
Step 4: Implementation
Enums and Constants
Copy
from enum import Enum
from datetime import datetime, timedelta
from typing import Optional, Dict, List
from dataclasses import dataclass, field
import uuid
class BookStatus(Enum):
AVAILABLE = 1
BORROWED = 2
RESERVED = 3
LOST = 4
class MemberStatus(Enum):
ACTIVE = 1
SUSPENDED = 2 # Too many overdue books
CLOSED = 3
class ReservationStatus(Enum):
PENDING = 1
FULFILLED = 2
CANCELLED = 3
EXPIRED = 4
# Library constants
MAX_BOOKS_PER_MEMBER = 5
LOAN_PERIOD_DAYS = 14
FINE_PER_DAY = 0.50
MAX_RESERVATION_DAYS = 7
Book Classes
Copy
@dataclass
class RackLocation:
floor: int
section: str
shelf: int
@dataclass
class Book:
isbn: str
title: str
author: str
publisher: str
publication_year: int
subject: str
copies: List['BookCopy'] = field(default_factory=list)
def add_copy(self, copy: 'BookCopy'):
self.copies.append(copy)
def get_available_copy(self) -> Optional['BookCopy']:
"""Get first available copy"""
for copy in self.copies:
if copy.status == BookStatus.AVAILABLE:
return copy
return None
def available_count(self) -> int:
return sum(1 for c in self.copies if c.status == BookStatus.AVAILABLE)
def total_count(self) -> int:
return len(self.copies)
class BookCopy:
def __init__(self, book: Book, rack: RackLocation):
self.barcode = str(uuid.uuid4())[:8].upper()
self.book = book
self.rack = rack
self.status = BookStatus.AVAILABLE
self.borrowed_by: Optional[str] = None # Member ID
self.due_date: Optional[datetime] = None
def checkout(self, member_id: str) -> bool:
if self.status != BookStatus.AVAILABLE:
return False
self.status = BookStatus.BORROWED
self.borrowed_by = member_id
self.due_date = datetime.now() + timedelta(days=LOAN_PERIOD_DAYS)
return True
def checkin(self) -> Optional[float]:
"""Return book and calculate fine if overdue"""
if self.status != BookStatus.BORROWED:
return None
fine = 0.0
if datetime.now() > self.due_date:
overdue_days = (datetime.now() - self.due_date).days
fine = overdue_days * FINE_PER_DAY
self.status = BookStatus.AVAILABLE
self.borrowed_by = None
self.due_date = None
return fine
Member Class
Copy
class Member:
def __init__(self, name: str, email: str, phone: str):
self.id = str(uuid.uuid4())[:8].upper()
self.name = name
self.email = email
self.phone = phone
self.status = MemberStatus.ACTIVE
self.borrowed_books: List[BookCopy] = []
self.reservations: List['Reservation'] = []
self.total_fines = 0.0
self.join_date = datetime.now()
def can_borrow(self) -> bool:
"""Check if member can borrow more books"""
if self.status != MemberStatus.ACTIVE:
return False
if len(self.borrowed_books) >= MAX_BOOKS_PER_MEMBER:
return False
if self.total_fines > 10.0: # Outstanding fines limit
return False
return True
def borrow_book(self, book_copy: BookCopy) -> Optional['Lending']:
"""Borrow a book"""
if not self.can_borrow():
raise Exception("Cannot borrow: limit reached or account suspended")
if not book_copy.checkout(self.id):
return None
self.borrowed_books.append(book_copy)
return Lending(
id=str(uuid.uuid4()),
member_id=self.id,
book_copy=book_copy,
borrow_date=datetime.now(),
due_date=book_copy.due_date
)
def return_book(self, book_copy: BookCopy) -> float:
"""Return a book and get fine amount"""
if book_copy not in self.borrowed_books:
raise Exception("Book not borrowed by this member")
fine = book_copy.checkin()
self.borrowed_books.remove(book_copy)
if fine > 0:
self.total_fines += fine
return fine
def pay_fine(self, amount: float) -> bool:
"""Pay outstanding fines"""
if amount <= 0 or amount > self.total_fines:
return False
self.total_fines -= amount
# Reactivate if suspended and fines cleared
if self.status == MemberStatus.SUSPENDED and self.total_fines == 0:
self.status = MemberStatus.ACTIVE
return True
Lending and Reservation
Copy
@dataclass
class Lending:
id: str
member_id: str
book_copy: BookCopy
borrow_date: datetime
due_date: datetime
return_date: Optional[datetime] = None
fine_amount: float = 0.0
def is_overdue(self) -> bool:
if self.return_date:
return False
return datetime.now() > self.due_date
def days_overdue(self) -> int:
if not self.is_overdue():
return 0
return (datetime.now() - self.due_date).days
@dataclass
class Reservation:
id: str
member_id: str
book: Book
reservation_date: datetime
status: ReservationStatus = ReservationStatus.PENDING
expiry_date: datetime = None
def __post_init__(self):
if self.expiry_date is None:
self.expiry_date = self.reservation_date + timedelta(days=MAX_RESERVATION_DAYS)
def is_expired(self) -> bool:
return datetime.now() > self.expiry_date
def fulfill(self):
self.status = ReservationStatus.FULFILLED
def cancel(self):
self.status = ReservationStatus.CANCELLED
Library System
Copy
import threading
from abc import ABC, abstractmethod
class SearchStrategy(ABC):
@abstractmethod
def search(self, books: Dict[str, Book], query: str) -> List[Book]:
pass
class TitleSearchStrategy(SearchStrategy):
def search(self, books: Dict[str, Book], query: str) -> List[Book]:
return [b for b in books.values() if query.lower() in b.title.lower()]
class AuthorSearchStrategy(SearchStrategy):
def search(self, books: Dict[str, Book], query: str) -> List[Book]:
return [b for b in books.values() if query.lower() in b.author.lower()]
class ISBNSearchStrategy(SearchStrategy):
def search(self, books: Dict[str, Book], query: str) -> List[Book]:
book = books.get(query)
return [book] if book else []
class Library:
def __init__(self, name: str):
self.name = name
self.books: Dict[str, Book] = {} # ISBN -> Book
self.members: Dict[str, Member] = {} # Member ID -> Member
self.lendings: Dict[str, Lending] = {} # Lending ID -> Lending
self.reservations: List[Reservation] = []
self._lock = threading.Lock()
# Book Management
def add_book(self, book: Book) -> bool:
with self._lock:
if book.isbn not in self.books:
self.books[book.isbn] = book
return True
return False
def add_book_copy(self, isbn: str, rack: RackLocation) -> Optional[BookCopy]:
with self._lock:
book = self.books.get(isbn)
if book is None:
return None
copy = BookCopy(book, rack)
book.add_copy(copy)
return copy
def search(self, query: str, strategy: SearchStrategy) -> List[Book]:
return strategy.search(self.books, query)
# Member Management
def register_member(self, name: str, email: str, phone: str) -> Member:
member = Member(name, email, phone)
self.members[member.id] = member
return member
# Lending Operations
def checkout_book(self, member_id: str, isbn: str) -> Optional[Lending]:
with self._lock:
member = self.members.get(member_id)
book = self.books.get(isbn)
if member is None or book is None:
return None
# Check for reservation by this member
self._fulfill_reservation_if_exists(member, book)
# Get available copy
copy = book.get_available_copy()
if copy is None:
return None
lending = member.borrow_book(copy)
if lending:
self.lendings[lending.id] = lending
return lending
def return_book(self, member_id: str, barcode: str) -> float:
with self._lock:
member = self.members.get(member_id)
if member is None:
raise Exception("Member not found")
# Find the book copy
book_copy = None
for copy in member.borrowed_books:
if copy.barcode == barcode:
book_copy = copy
break
if book_copy is None:
raise Exception("Book not found in member's borrowed list")
fine = member.return_book(book_copy)
# Check for pending reservations
self._notify_next_reservation(book_copy.book)
return fine
# Reservation Operations
def reserve_book(self, member_id: str, isbn: str) -> Optional[Reservation]:
with self._lock:
member = self.members.get(member_id)
book = self.books.get(isbn)
if member is None or book is None:
return None
# Check if book is available (no need to reserve)
if book.get_available_copy() is not None:
return None # Can borrow directly
# Check if already reserved by this member
for res in self.reservations:
if res.member_id == member_id and res.book.isbn == isbn:
if res.status == ReservationStatus.PENDING:
return None # Already reserved
reservation = Reservation(
id=str(uuid.uuid4()),
member_id=member_id,
book=book,
reservation_date=datetime.now()
)
self.reservations.append(reservation)
member.reservations.append(reservation)
return reservation
def _fulfill_reservation_if_exists(self, member: Member, book: Book):
for res in member.reservations:
if res.book.isbn == book.isbn and res.status == ReservationStatus.PENDING:
res.fulfill()
break
def _notify_next_reservation(self, book: Book):
"""Notify next person in reservation queue"""
for res in self.reservations:
if res.book.isbn == book.isbn and res.status == ReservationStatus.PENDING:
if not res.is_expired():
self._send_notification(res.member_id, book)
break
else:
res.status = ReservationStatus.EXPIRED
def _send_notification(self, member_id: str, book: Book):
member = self.members.get(member_id)
if member:
print(f"Notification sent to {member.email}: "
f"'{book.title}' is now available for pickup")
# Reports
def get_overdue_books(self) -> List[Lending]:
return [l for l in self.lendings.values() if l.is_overdue()]
def get_member_history(self, member_id: str) -> List[Lending]:
return [l for l in self.lendings.values() if l.member_id == member_id]
Step 5: Usage Example
Copy
# Create library
library = Library("City Central Library")
# Add books
book1 = Book(
isbn="978-0-13-468599-1",
title="Clean Code",
author="Robert C. Martin",
publisher="Prentice Hall",
publication_year=2008,
subject="Software Engineering"
)
library.add_book(book1)
# Add multiple copies
rack = RackLocation(floor=2, section="CS", shelf=3)
library.add_book_copy("978-0-13-468599-1", rack)
library.add_book_copy("978-0-13-468599-1", rack)
# Register member
member = library.register_member(
name="John Doe",
email="[email protected]",
phone="555-1234"
)
# Search for book
results = library.search("Clean Code", TitleSearchStrategy())
print(f"Found {len(results)} books")
# Borrow book
lending = library.checkout_book(member.id, "978-0-13-468599-1")
print(f"Borrowed: {lending.book_copy.book.title}, Due: {lending.due_date}")
# Return book (simulating after due date for fine)
import time
time.sleep(1)
fine = library.return_book(member.id, lending.book_copy.barcode)
print(f"Fine: ${fine:.2f}")
Key Design Decisions
Why separate Book and BookCopy?
Why separate Book and BookCopy?
A book (metadata) can have multiple physical copies. Each copy has its own barcode, status, and location. This allows tracking individual copies while sharing common book information.
Why Strategy Pattern for Search?
Why Strategy Pattern for Search?
Different search criteria (title, author, ISBN, subject) have different logic. Strategy pattern allows adding new search types without modifying the Library class.
Why Queue-based Reservations?
Why Queue-based Reservations?
When a book is returned, the person who reserved first should be notified first. A queue (list with FIFO behavior) naturally handles this fairness.
Interview Extension: Be ready to discuss handling book categories, late fees with grace periods, librarian vs member permissions, or integration with an online catalog.