Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

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. It naturally introduces the critical distinction between metadata and physical instances (Book vs. BookCopy), queue-based reservations (fairness in resource allocation), and the Strategy pattern for search. Despite being labeled “beginner,” a well-executed Library design with proper separation of concerns will impress interviewers because it shows you think about real-world constraints, not just textbook examples.

📋 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

CategoryQuestionImpact on Design
BooksMultiple copies of same book?BookCopy vs Book
MembersDifferent member types?Member hierarchy
LimitsMax books per member? Loan period?Validation logic
ReservationsCan reserve borrowed books?Reservation queue
FinesHow are fines calculated?Fine calculator
NotificationsDue 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

EntityResponsibilitiesPattern
LibraryManage catalog, coordinate operationsSingleton
BookStore metadata, track copies-
BookCopyTrack status, location-
MemberBorrow, return, pay fines-
LendingTrack borrow date, due date-
NotificationServiceDue date remindersObserver

Key Constraints

MAX_BOOKS_PER_MEMBER = 5
LOAN_PERIOD_DAYS = 14
FINE_PER_DAY = 0.50
MAX_RESERVATION_DAYS = 7

📐 Step 3: Class Diagram

┌─────────────────────────────────────────────────────────────┐
│                         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

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

@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

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

@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

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

# 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="john@example.com",
    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

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. This is a real-world modeling pattern that appears everywhere: a Movie has many Screenings, a Product has many StockItems, a Song has many AudioFiles in different formats. The distinction matters because operations differ: you search by Book metadata but borrow a specific BookCopy. If you merge them, you cannot answer “how many copies of Clean Code are available?” without scanning through all lending records. In database terms, this is normalization — separating the entity from its instances.
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 Deep-Dive Questions

Strong answer:
  • On the surface, direct methods seem simpler — and for a system with only 3 search types, they arguably are. The Strategy pattern earns its keep when you anticipate combinatorial growth in search behavior. Today you search by title, author, ISBN. Tomorrow the librarian wants full-text search across descriptions, search by publication year range, search by subject and availability, or a fuzzy/typo-tolerant search. Each of those is a new class, not a modification to Library. Direct methods would mean Library grows a new method for every search variant, violating the Open/Closed Principle.
  • The deeper value is composability. With Strategy objects, you can build a CompositeSearchStrategy that combines multiple strategies with AND/OR logic: “books by Robert Martin published after 2005 that are currently available.” Try doing that with hardcoded methods and you end up with a combinatorial explosion of search_by_title_and_author_and_year() methods.
  • The tradeoff is real, though — for a small library with three search types, Strategy adds indirection that a junior developer must trace through. Pragmatically, you might start with direct methods and refactor to Strategy when the fourth or fifth search type arrives. This is a judgment call, not a dogma.
  • Example: A university library system I worked with needed to add “search by course syllabus” — given a course ID, find all books on the reading list. With Strategy, that was one new class implementing SearchStrategy. Without it, that would have been a new method on Library plus new database queries interwoven with existing code.
Red flag answer: “Strategy pattern is always better because it follows SOLID” — this is pattern worship without pragmatic judgment. A strong candidate acknowledges that patterns have a cost (indirection, abstraction overhead) and earns their use with a concrete scenario. Blindly applying patterns to a 3-method class is over-engineering.Follow-ups:
  1. The current search strategies do linear scans over books.values(). If the library has 500,000 books, how would you optimize this — and would you still use the Strategy pattern or move to a different architecture entirely (e.g., an inverted index, Elasticsearch)?
  2. How would you implement a “search by availability” strategy that needs to check BookCopy status, not just Book metadata? Does this break the current strategy interface, and if so, how would you refactor it?
Strong answer:
  • The fine calculation lives in BookCopy.checkin(). It compares datetime.now() to self.due_date. If current time is past due date, it calculates overdue_days = (datetime.now() - self.due_date).days and multiplies by FINE_PER_DAY (0.50).For10dayslate,thatis0.50). For 10 days late, that is 5.00. The fine is added to member.total_fines, and if total fines exceed $10.00, the member cannot borrow more books until they pay.
  • Edge case 1 — Partial day rounding: .days truncates to whole days. A book returned 23 hours and 59 minutes late shows 0 days overdue and incurs no fine. A book returned 24 hours and 1 minute late shows 1 day. This is arguably fair (grace period of ~1 day) but is not intentional design — it is an accident of timedelta.days behavior.
  • Edge case 2 — No fine cap: A member who loses a book and never returns it accumulates unbounded fines. There should be a maximum fine (e.g., the replacement cost of the book). Real libraries cap fines at the book’s value.
  • Edge case 3 — No grace period: Some libraries offer a 1-3 day grace period before fines kick in. The code has no concept of this.
  • Edge case 4 — Library closure days: If the library is closed for a holiday and the due date falls on that day, the member cannot return the book and is penalized for something outside their control. Real systems extend the due date to the next open day.
  • Edge case 5 — Fine timing inconsistency: The fine is calculated at checkin time using datetime.now(). If the member returns the book in the morning vs. the evening, the fine amount could differ. A date-only comparison (ignoring time) would be more fair.
  • Example: A member borrows a book, due date is Friday. Library is closed Saturday/Sunday. Member returns Monday morning. The code charges 3 days of fines ($1.50) for 2 days the library was closed.
Red flag answer: “It just checks if the return date is after the due date and multiplies by the daily rate” — this is a literal restatement of the code with zero critical analysis. The interviewer is testing whether you can think beyond the happy path and identify real-world fairness and correctness issues.Follow-ups:
  1. How would you implement a tiered fine system where the first 3 days are 0.25/day,days414are0.25/day, days 4-14 are 0.50/day, and beyond 14 days the item is marked “lost” and the member is charged replacement cost? Where does this logic live — BookCopy, Member, or a new FineCalculator class?
  2. The member disputes a fine claiming they returned the book on time but the librarian scanned it late. How would you design an audit trail to resolve disputes — what events would you log and what timestamps matter?
Strong answer:
  • Reservation flow: Member calls Library.reserve_book(). The code first checks if an available copy exists — if so, returns None because the member should just borrow it directly. Otherwise, it checks for duplicate pending reservations by the same member for the same book. If everything passes, a Reservation object is created with an expiry date (7 days from now) and appended to both self.reservations (library-wide list) and member.reservations.
  • Fulfillment flow: When someone returns a book, Library.return_book() calls _notify_next_reservation(book). This iterates the reservation list and finds the first PENDING reservation for that book. If it is not expired, the member is notified. If expired, it is marked EXPIRED and the next reservation is checked. When the notified member comes to check out, _fulfill_reservation_if_exists marks their reservation as FULFILLED.
  • Fairness issue 1 — No hold period after notification: The member is notified but there is no mechanism to hold a copy for them. Between notification and arrival, someone else could borrow the available copy. The reservation becomes meaningless.
  • Fairness issue 2 — No copy reservation: The available copy is not linked to the reservation. If there are 2 copies and 3 reservations, returning one copy notifies the first reserver, but any member can borrow the copy before the reserver arrives.
  • Fairness issue 3 — The list is not sorted: self.reservations is a flat list. The linear scan finds the “first” reservation, but insertion order depends on when members called reserve_book, not on any priority system. A proper queue (or list sorted by reservation_date) would be clearer.
  • Example: Member A reserves “Clean Code” on Monday. Copy is returned Wednesday. Member A is notified via email. Member B walks in Wednesday afternoon, sees the copy on the shelf, and borrows it before Member A arrives Thursday. Member A’s reservation was effectively useless. A real system would mark the copy as “ON HOLD for 48 hours” and prevent anyone else from borrowing it.
Red flag answer: “Reservations just send a notification and the member picks up the book” — this ignores the gap between notification and pickup where the copy is unprotected. It is the classic “happy path only” answer that would break immediately in a real library.Follow-ups:
  1. How would you implement a “hold shelf” mechanism where a returned copy is set aside for the first reserver for 48 hours before becoming generally available? What new status would you add to BookStatus and where would the timeout logic run?
  2. If the library system serves 10,000 members and a popular book has 200 reservations, the linear scan in _notify_next_reservation becomes expensive. How would you redesign the reservation storage for O(1) “next reserver” lookups?
Strong answer:
  • Define a NotificationChannel abstract class with a send(member, message) method. Implement EmailNotificationChannel, SMSNotificationChannel, and InAppNotificationChannel. Members have a notification_preferences field listing which channels they have opted into. When a notification event occurs (book available, due date approaching, fine accrued), the system iterates the member’s preferred channels and dispatches through each.
  • The Observer pattern fits naturally: notification-triggering events (book returned, reservation expiring, due date approaching) are the “subjects,” and the notification channels are the “observers.” But the key architectural decision is where the event is detected vs. where the notification is sent. Event detection lives in the domain logic (Library, Lending). Notification dispatch lives in a separate NotificationService that the Library calls. This prevents domain classes from knowing about email APIs or SMS gateways.
  • For due-date reminders specifically, you need a background scheduled job (cron, Celery, or similar) that runs daily, scans all active lendings, and sends reminders for books due in 1-3 days. This is fundamentally different from event-driven notifications — it is poll-based. Both patterns coexist in a real system.
  • Example: A member has opted into email and in-app but not SMS. When their reserved book becomes available, NotificationService sends an email via SendGrid and creates an in-app notification record in the database. If SendGrid is down, the system should retry with exponential backoff or fall back to SMS as a degraded mode — not silently fail.
Red flag answer: “Just add send_email() calls in the Library class wherever something happens” — this scatters notification logic across the entire codebase, makes it impossible to test Library without an email server, and ensures that adding a new notification channel requires modifying every call site. It is the exact problem Observer solves.Follow-ups:
  1. How do you handle notification failures (email bounces, SMS delivery fails) without blocking the core library operation? Should returning a book fail if the notification to the next reserver cannot be sent?
  2. Members complain about getting too many notifications. How would you design a notification batching or digest system — for example, “send me one daily summary instead of 5 individual emails”?
Strong answer:
  • The current design makes Member responsible for knowing library business rules — the max book limit (5), the fine threshold ($10), and the status check. These are library policies, not member properties. If the library decides to change the max from 5 to 7, or offer premium members a limit of 10, you are editing the Member class — which should only model a member’s identity and state, not encode business rules.
  • A cleaner design extracts a BorrowingPolicy class (or set of policy objects) that the Library consults during checkout. The policy takes a member and returns a borrow-eligibility decision with a reason. StandardBorrowingPolicy checks the 5-book limit and 10finethreshold.PremiumBorrowingPolicyallows10booksand10 fine threshold. `PremiumBorrowingPolicy` allows 10 books and 25 fine threshold. StudentBorrowingPolicy allows 3 books but no fine threshold (university pays). The Library selects the appropriate policy based on member type.
  • This also makes the system testable in isolation. You can test StandardBorrowingPolicy without creating a real Member object with borrowed books. You can test Member without hardcoded constants.
  • Example: During exam season, the library temporarily raises the max books from 5 to 8 for all members. With the current design, you change a constant in Member (risky deploy). With a policy object, you swap in a ExamSeasonBorrowingPolicy — no code change to Member at all.
Red flag answer: “It is fine where it is because Member needs to know if it can borrow” — this conflates who needs the answer (Member) with who owns the rule (Library/Policy). A member should know how many books it has; the library should decide the limit.Follow-ups:
  1. If different branches of the same library system have different borrowing limits (downtown allows 5, suburban allows 7), where does the branch-specific policy live? How does the Library class select the right policy at checkout time?
  2. The total_fines > 10.0 check uses a floating-point comparison. What can go wrong with accumulated floating-point fines (e.g., 20 returns of $0.50 each), and how would you fix this?
Strong answer:
  • The separation reflects a real-world truth: a library can own 15 copies of “Clean Code” and each copy has independent state — one is borrowed, one is on the hold shelf, one is lost, the rest are available. The Book class stores shared, immutable metadata (ISBN, title, author) exactly once. Each BookCopy stores instance-specific mutable state (barcode, status, due date, rack location). Without this split, you would duplicate metadata 15 times and risk inconsistency (what if one copy says author is “Robert Martin” and another says “Bob Martin”?).
  • From a data modeling perspective, this is a one-to-many relationship — one of the most fundamental patterns in system design. Collapsing it means you either cannot track individual copies (you just know “5 are available” as a count, but not which physical copy is where) or you duplicate metadata per copy. Tracking individual copies matters for: locating a specific copy on a shelf, auditing who had which copy, and handling damaged/lost copies without affecting the book’s overall catalog entry.
  • The separation also enables a clean get_available_copy() method on Book that iterates copies and returns the first available one. If books and copies were merged, “find an available copy” becomes “find another row in the same table with the same ISBN and AVAILABLE status” — doable but loses the containment relationship.
  • Example: A member reports Book Copy #A3F2 of “Design Patterns” is missing pages. The librarian marks that specific copy as DAMAGED without affecting the other 4 copies. If Book and BookCopy were one class, marking damage would require a separate “damage” field per row and careful filtering everywhere.
Red flag answer: “I would just use one Book class with a count field for how many copies exist” — this is the classic mistake. A count tells you nothing about where each copy is, who has it, or its condition. You cannot track individual barcodes, due dates, or rack locations with a counter.Follow-ups:
  1. If the library switches to eBooks where there is no physical copy but a license count (e.g., “3 concurrent readers allowed”), how would you model this? Does the Book/BookCopy split still apply, or do you need a different abstraction?
  2. A librarian wants to transfer 5 copies of a book from one branch to another. How does the current model support (or fail to support) multi-branch inventory management?
Strong answer:
  • Step 1 — Borrow attempt: The member calls checkout_book. The Library acquires the lock, finds the member and book. _fulfill_reservation_if_exists checks if the member has a pending reservation for this specific book — if so, marks it FULFILLED. Then book.get_available_copy() finds a copy. member.borrow_book(copy) calls can_borrow().
  • Step 2 — can_borrow() fails: Status is ACTIVE (pass). len(borrowed_books) = 3 < 5 (pass). But total_fines = 12.0 > 10.0 (fail). The method returns False. borrow_book raises an exception: “Cannot borrow: limit reached or account suspended.” The checkout returns None.
  • Step 3 — Meanwhile, the reservation: The other book was returned by someone else. _notify_next_reservation found this member’s pending reservation and called _send_notification. But the member still cannot check out that reserved book because can_borrow() will fail again for the same fine-threshold reason. The reserved copy sits on the shelf, eventually the reservation expires after 7 days, and the next reserver is notified.
  • The fairness problem: The member is punished twice — they cannot borrow and their reservation effectively expires without them being able to act on it. A better design would either: (a) extend the reservation expiry while the member has outstanding fines, or (b) allow fine payment to immediately re-check pending reservations.
  • Example: This exact scenario causes customer complaints in real libraries. The member pays their $12 fine but by then the 7-day reservation window has passed. The system should trigger a “re-evaluate reservations” check when fines are paid below the threshold.
Red flag answer: Answering only the borrow attempt without considering the reservation interaction. The question deliberately includes both to test whether the candidate can trace through multiple interacting flows and spot emergent problems at the intersection.Follow-ups:
  1. The can_borrow check uses a hard threshold of total_fines > 10.0. Should reservation fulfillment bypass this check since the member already “earned” the book by reserving it? What are the arguments for and against?
  2. If pay_fine reduces total_fines below $10 and reactivates a suspended member, should the system automatically attempt to fulfill any pending reservations at that moment? How would you implement this without creating circular dependencies between pay_fine and checkout_book?
Interview Extension: Be ready to discuss handling book categories, late fees with grace periods, librarian vs member permissions, or integration with an online catalog.