LLD: Library Management System
Books versus copies, members, loans, holds, and fines — a domain-modelling exercise where getting the nouns right is most of the work.
Big idea
The key insight in a library system is that a Book (the title, ISBN, author) is not a Copy (the physical item on the shelf with a barcode). Members borrow copies; they place holds on books. Get that split right, put each rule with the object that owns it — loan limits on the member, due dates on the loan, availability on the copy — and the rest falls into place.
Requirements
Functional
- Catalogue: add books and their copies; search by title or author.
- Members borrow available copies (max 3 at a time), return them, and renew once if nobody is waiting.
- Overdue returns accrue a fine per day; members with unpaid fines over a threshold can't borrow.
- Holds: if no copy is available, a member joins a queue for that book; a returned copy goes to the first person waiting.
Out of scope: payments, notifications delivery (we'll publish events), multiple branches (extension).
Entities
Library (facade over the use cases), Catalog, Book (isbn, title, author), Copy (barcode, book, status), Member (id, loans, fines), Loan (copy, member, due date, returned), HoldQueue per book, FinePolicy (strategy), Clock.
Think of it like
A record shop's rental counter. The catalogue binder lists albums (books); the shelf behind the counter holds the actual discs (copies), each with a sticker number. Your membership card shows what you have out. If every copy of an album is out, your name goes on a list taped to its divider, and the next returned disc is set aside for whoever is first on the list.
Class diagram
Key flow: a return that fulfils a hold
Code skeleton
from collections import deque
class Book:
def __init__(self, isbn, title):
self.isbn, self.title, self.copies, self.holds = isbn, title, [], deque()
class Copy:
def __init__(self, barcode, book):
self.barcode, self.book, self.status, self.held_for = barcode, book, "available", None
class Member:
MAX_LOANS, MAX_FINES = 3, 10
def __init__(self, mid):
self.id, self.loans, self.fines = mid, [], 0
def can_borrow(self):
return len(self.loans) < self.MAX_LOANS and self.fines <= self.MAX_FINES
class Loan:
PERIOD = 14
def __init__(self, copy, member, today):
self.copy, self.member, self.due = copy, member, today + self.PERIOD
class PerDayFine: # FinePolicy strategy
def fine(self, loan, today):
return max(0, today - loan.due) * 1
class Library:
def __init__(self, fine_policy, clock):
self.books, self.copies, self.members, self.loans = {}, {}, {}, {}
self.fines, self.clock = fine_policy, clock
def add_book(self, isbn, title, barcodes):
book = self.books[isbn] = Book(isbn, title)
for b in barcodes:
book.copies.append(self.copies.setdefault(b, Copy(b, book)))
def borrow(self, mid, isbn):
member, book = self.members[mid], self.books[isbn]
if not member.can_borrow():
return f"{mid}: not allowed (loans {len(member.loans)}, fines {member.fines})"
copy = next((c for c in book.copies if c.status == "available"
or (c.status == "on hold" and c.held_for == mid)), None)
if copy is None:
book.holds.append(mid)
return f"{mid}: no copy free, hold placed (position {len(book.holds)})"
loan = self.loans[copy.barcode] = Loan(copy, member, self.clock())
copy.status, copy.held_for = "on loan", None
member.loans.append(loan)
return f"{mid}: borrowed {copy.barcode}, due day {loan.due}"
def give_back(self, barcode):
loan = self.loans.pop(barcode)
fine = self.fines.fine(loan, self.clock())
loan.member.fines += fine
loan.member.loans.remove(loan)
copy = loan.copy
if copy.book.holds: # hand it to the first in line
copy.status, copy.held_for = "on hold", copy.book.holds.popleft()
else:
copy.status = "available"
return f"{loan.member.id} returned {barcode}, fine {fine}, copy now {copy.status}" + (
f" for {copy.held_for}" if copy.held_for else "")
today = [1]
lib = Library(PerDayFine(), clock=lambda: today[0])
lib.add_book("978-1", "Clean Structures", ["C1"])
for mid in ("ana", "ben", "cy"):
lib.members[mid] = Member(mid)
print(lib.borrow("ana", "978-1"))
print(lib.borrow("ben", "978-1"))
print(lib.borrow("cy", "978-1"))
today[0] = 18 # three days late
print(lib.give_back("C1"))
print(lib.borrow("cy", "978-1")) # cy isn't first in line
print(lib.borrow("ben", "978-1"))output
ana: borrowed C1, due day 15
ben: no copy free, hold placed (position 1)
cy: no copy free, hold placed (position 2)
ana returned C1, fine 3, copy now on hold for ben
cy: no copy free, hold placed (position 2)
ben: borrowed C1, due day 32Run and edit it in the app.
Watch it
Why separate Book from Copy?
Where should the "at most 3 loans and fines under 10" rule live?
Extensions and trade-offs
- Hold expiry — a held copy waits N days, then goes to the next in line (a scheduled job).
- Notifications — publish
HoldReadyandOverdueevents (Observer); email/SMS are subscribers. - Different fine rules for children or reference books — more
FinePolicyimplementations. - Branches — copies gain a location; transfers between branches become a flow of their own.
- Concurrency — two members borrowing the last copy at once: claim the copy atomically (as in Parking lot).
Key terms
- Copy (item) — One physical instance of a book, with its own barcode and status.
- Hold — A member's place in the queue for a book with no free copies.
- Fine policy — The swappable rule that turns lateness into a charge.