> ## 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.

# Case Study: Library Management System

> Complete low-level design for a library management system

<Info>
  **Difficulty**: 🟢 Beginner | **Time**: 35-45 minutes | **Patterns**: Singleton, Observer, Factory
</Info>

## 🎯 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

<Tip>
  **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.
</Tip>

***

## 📋 Step 1: Clarify Requirements

<Warning>
  **Interview Tip**: Library problems can be simple or complex. Clarify if you need multiple copies, reservations, and notifications.
</Warning>

### 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

<Note>
  **Key Insight**: Distinguish between `Book` (metadata - title, author, ISBN) and `BookCopy` (physical copy - barcode, status). One book can have many copies!
</Note>

<CardGroup cols={3}>
  <Card title="Books" icon="book">
    Book, BookCopy, Rack Location
  </Card>

  <Card title="Users" icon="users">
    Member, Librarian, Account
  </Card>

  <Card title="Transactions" icon="exchange">
    Lending, Reservation, Fine
  </Card>
</CardGroup>

### 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

```python theme={null}
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

```python theme={null}
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

```python theme={null}
@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

```python theme={null}
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

```python theme={null}
@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

```python theme={null}
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

```python theme={null}
# 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

<AccordionGroup>
  <Accordion title="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. 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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

## Interview Deep-Dive Questions

<AccordionGroup>
  <Accordion title="Q1: The search system uses the Strategy pattern with TitleSearchStrategy, AuthorSearchStrategy, etc. Why not just add search_by_title(), search_by_author() methods directly on Library? When does the Strategy pattern actually earn its keep here?">
    **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?
  </Accordion>

  <Accordion title="Q2: Two members both want the last available copy of a popular book. Walk me through exactly how the code prevents a race condition where both successfully borrow it.">
    **Strong answer:**

    * The `Library.checkout_book()` method is wrapped in `with self._lock`, a `threading.Lock()`. This means only one thread can execute the checkout flow at a time. When Member A enters checkout, the lock is acquired. Member A's call to `book.get_available_copy()` finds the copy, and `member.borrow_book(copy)` calls `copy.checkout(member_id)` which flips the status to `BORROWED`. When Member B's thread gets the lock next, `book.get_available_copy()` returns `None` because the status is already `BORROWED`, and checkout returns `None`.
    * The critical design choice is that the lock is at the `Library` level, not the `BookCopy` level. This is a coarse-grained lock — simple and correct, but it means *all* checkout operations are serialized, even for completely unrelated books. If two members are borrowing different books simultaneously, one waits. For a library with dozens of concurrent checkouts, this becomes a bottleneck.
    * A finer-grained approach would lock per-ISBN or per-BookCopy. But then you risk deadlocks if a single operation needs to touch multiple books (e.g., "borrow these 3 books together"). The current approach trades throughput for simplicity and correctness — a valid choice for a system with modest concurrency.
    * **Example:** Imagine a university library during the first week of semester — 200 students all trying to check out the same 10 textbooks. The coarse lock means each checkout takes \~1ms but they are fully serialized: 200 \* 1ms = 200ms total. A per-book lock would allow parallel checkouts for different books but would be more complex to reason about.

    **Red flag answer:** "Python has the GIL so you don't need locks" — the GIL prevents simultaneous Python bytecode execution across threads but does NOT prevent race conditions. A context switch can happen between checking availability and updating status. The GIL protects CPython internals, not your application logic. This is one of the most common Python concurrency misconceptions.

    **Follow-ups:**

    1. What if this system were a web app with multiple server processes (not threads)? The `threading.Lock` would be useless across processes. How would you handle concurrency — database-level locking, optimistic concurrency control, or something else?
    2. The current code acquires the lock before checking if the member or book exists. If validation fails (member not found), the lock was held unnecessarily. How would you restructure the method to minimize lock contention while still preventing double-checkout?
  </Accordion>

  <Accordion title="Q3: A member returns a book 10 days late. Walk through the fine calculation, and then tell me every edge case or unfairness you can see in the current implementation.">
    **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). 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, 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?
  </Accordion>

  <Accordion title="Q4: A member reserves a book that is currently checked out. Walk through what happens from reservation to fulfillment, and identify the concurrency and fairness issues in the current design.">
    **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?
  </Accordion>

  <Accordion title="Q5: The notification system currently just prints to console. How would you design a real notification system using the Observer pattern that supports email, SMS, and in-app notifications?">
    **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"?
  </Accordion>

  <Accordion title="Q6: The Member class has can_borrow() which checks three conditions: active status, book limit, and outstanding fines. What is wrong with putting all this validation in the Member class, and how would you redesign it?">
    **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 $10 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?
  </Accordion>

  <Accordion title="Q7: The current design uses Book (metadata) and BookCopy (physical instance). Why is this separation critical, and what breaks if you collapse them into a single class?">
    **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?
  </Accordion>

  <Accordion title="Q8: The system needs to handle a member who has 3 overdue books, $12 in fines, and tries to borrow a new book while also having an active reservation for a different book that just became available. Walk through every check and state transition that occurs.">
    **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`?
  </Accordion>
</AccordionGroup>

<Tip>
  **Interview Extension**: Be ready to discuss handling book categories, late fees with grace periods, librarian vs member permissions, or integration with an online catalog.
</Tip>
