🧠 Library Management LLD — Thought Process Guide¶
Goal: Learn how to think when designing a Low-Level Design.
📊 Class Diagram¶
classDiagram
direction LR
class LibraryService {
+HOLD_DAYS: int
-_catalog: Catalog
-_fines: FineCalculator
-_clock: Callable
-_members: Dict~str, Member~
-_loans: Dict~str, Loan~
-_active: Dict~str, Loan~
-_waitlist: Dict~str, Deque~
-_holds: Dict~str, Hold~
+register_member(name: str, member_type: MemberType) Member
+member(member_id: str) Member
+available_copies(isbn: str) int
+borrow(member_id: str, isbn: str) Loan
+return_item(barcode: str) Loan
+renew(barcode: str) Loan
+report_lost(barcode: str, replacement_cost: Decimal) Loan
+reserve(member_id: str, isbn: str) int
+cancel_reservation(member_id: str, isbn: str) None
+hold_for(member_id: str, isbn: str) Optional~Hold~
+pay_fine(member_id: str, amount: Decimal) Decimal
}
class Catalog {
-_books: Dict~str, Book~
-_items: Dict~str, BookItem~
+add_book(book: Book) None
+add_item(item: BookItem) None
+book(isbn: str) Book
+item(barcode: str) BookItem
+items_for(isbn: str) List~BookItem~
+search(title: str, author: str, category: str) List~Book~
}
class Book {
<<dataclass>>
+isbn: str
+title: str
+author: str
+category: str
+year: int
}
class BookItem {
<<dataclass>>
+barcode: str
+isbn: str
+rack: str
+status: ItemStatus
}
class Loan {
<<dataclass>>
+loan_id: str
+barcode: str
+isbn: str
+member_id: str
+borrowed_on: date
+due_on: date
+renewals: int
+returned_on: Optional~date~
+fine: Decimal
+days_overdue(on: date) int
}
class Hold {
<<dataclass>>
+member_id: str
+barcode: str
+expires_on: date
}
class Member {
<<dataclass>>
+member_id: str
+name: str
+member_type: MemberType
+active_loans: Dict~str, Loan~
+fine_due: Decimal
+policy: MembershipPolicy
}
class MembershipPolicy {
<<dataclass>>
+max_books: int
+loan_days: int
+max_renewals: int
}
class ItemStatus {
<<enumeration>>
AVAILABLE
ON_LOAN
ON_HOLD
LOST
}
class MemberType {
<<enumeration>>
STUDENT
FACULTY
PUBLIC
PREMIUM
}
class FineCalculator {
<<abstract>>
+calculate_fine(days_overdue: int, member_type: MemberType) Decimal
}
class StandardFine {
+calculate_fine(days_overdue: int, member_type: MemberType) Decimal
}
class ProgressiveFine {
-_cap: Optional~Decimal~
+calculate_fine(days_overdue: int, member_type: MemberType) Decimal
}
FineCalculator <|-- StandardFine
FineCalculator <|-- ProgressiveFine
LibraryService o-- Catalog
LibraryService --> FineCalculator : prices overdue
LibraryService *-- "*" Member
LibraryService *-- "*" Loan : history
LibraryService *-- "*" Hold : barcode to hold
Catalog *-- "*" Book : by ISBN
Catalog *-- "*" BookItem : by barcode
Book "1" <.. "*" BookItem : isbn
BookItem --> ItemStatus
Member o-- "*" Loan : active_loans
Member --> MemberType
Member ..> MembershipPolicy : POLICIES lookup
Loan ..> BookItem : barcode
Hold ..> BookItem : barcode
⏱️ How to Run This in a 45–60 min Interview¶
| Time | Step | What to say out loud |
|---|---|---|
| 0–7 min | Clarify | "Member types and their limits? Fine rules? Do we need reservations, renewals, lost books? Single branch?" Pin down scope before drawing anything. |
| 7–15 min | Entities + interfaces | "Book is a title, BookItem is a copy. A Loan is its own record. Availability is derived from item status, so there is one source of truth." Name FineCalculator.calculate_fine as the strategy and POLICIES as the per-type table. |
| 15–35 min | Core code | Catalog with an ISBN → items index, then borrow / return_item with the borrowing checks (limit, fines, overdue). Inject the clock now; it costs one parameter and saves the testing discussion later. |
| 35–45 min | Reservations + concurrency | FIFO waitlist per ISBN, holds that expire, lazy expiry before availability decisions. One lock makes "find copy → mark on loan" atomic; in a DB that is a conditional UPDATE ... WHERE status = 'AVAILABLE'. |
| 45–60 min | Extension + tests | Renewals (blocked when someone is waiting), lost books, notifications. List the tests: fine tiers, limits per type, hold expiry passes to the next member, concurrent borrow issues each copy once. |
Clarifying questions worth asking
- How many books and for how long, per member type? Can loans be renewed, and how often?
- How are fines computed, is there a cap, and at what unpaid amount is borrowing blocked?
- Reservations: FIFO per title? How long is a hold kept before it passes on?
- What happens to a lost or damaged copy?
- One branch or many? Self-service kiosks (concurrent checkouts)?
Phase 0: Requirements Gathering¶
Members borrow copies of titles, subject to per-type limits and loan periods. Late returns accrue fines; unpaid fines or an overdue book block new loans. Titles with no copy on the shelf can be reserved; returned copies are held for the next member in line.
Phase 1: Identify the Nouns¶
"A library has books with multiple copies. Members borrow and return books. Fines accrue for overdue returns."
| Noun | Decision | Why |
|---|---|---|
| Book | frozen dataclass | Title metadata keyed by ISBN |
| BookItem | dataclass | A physical copy with a status |
| Loan | dataclass | One borrowing; due date, renewals, fine |
| Hold | dataclass | A copy reserved for one member until a date |
| Member | dataclass | Type, active loans, fine due |
| MembershipPolicy | frozen dataclass + table | Limits per member type |
| FineCalculator | ABC | Strategy for fines |
| Catalog | Regular class | Books, items, search |
| LibraryService | Facade | Orchestration, locking, clock |
Key insight: Book vs BookItem is title vs physical copy. Three copies of "Clean Code" are one Book and three BookItems. Do not also keep an "available copies" counter on Book; count the items.
Phase 2: Enums and Tables¶
class ItemStatus(Enum): AVAILABLE, ON_LOAN, ON_HOLD, LOST
class MemberType(Enum): STUDENT, FACULTY, PUBLIC, PREMIUM
POLICIES = {MemberType.STUDENT: MembershipPolicy(max_books=5, loan_days=14), ...}
Key the table by the enum member, not by a string. (The original code keyed it by "STUDENT" and looked it up with "Student", so every member got the default.)
Phase 3: Assigning Responsibilities¶
| Action | Owner | Why |
|---|---|---|
| Copy status | BookItem.status |
Single source of truth for availability |
| Can this member borrow? | LibraryService._check_can_borrow |
Needs member, policy, fines and clock |
| Calculate fine | FineCalculator.calculate_fine() |
Swappable rule |
| Search | Catalog.search() |
Owns the books and the ISBN index |
| Borrow / return / renew | LibraryService |
Orchestrates under one lock |
| Who gets a returned copy | LibraryService._release |
Queue head gets a hold, else shelf |
Phase 4: The Borrow and Return Flows¶
borrow(member_id, isbn):
1. member exists; under limit; fines below threshold; nothing overdue
2. expire stale holds for this isbn
3. pick the copy held for this member, else any AVAILABLE copy
4. create Loan(due = today + policy.loan_days); copy -> ON_LOAN
return_item(barcode):
1. close the Loan, compute fine from days overdue, add to member.fine_due
2. release the copy: ON_HOLD for the queue head (expires in HOLD_DAYS), else AVAILABLE
Phase 5: Strategy Pattern for Fines¶
class FineCalculator(ABC):
def calculate_fine(self, days_overdue: int, member_type: MemberType) -> Decimal
class StandardFine(FineCalculator): # flat daily rate by member type
class ProgressiveFine(FineCalculator): # 1/day to day 7, 2/day to day 30, 5/day after; optional cap
Phase 6: Quick Checklist¶
✅ Book vs BookItem, with availability derived from item status
✅ Loan as a record, so renewals, fines and history have a home
✅ Reservations are FIFO with expiring holds that pass to the next member
✅ Money is Decimal; time comes from an injected clock
✅ Concurrency: one lock around find-and-mark; conditional update in a DB
✅ Policy table keyed by enum; new member type = new row