Skip to content
FollowtheDamnCode

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

Books, copies, members, loans

Key flow: a return that fulfils a hold

Return, fine, and hand to the next in line

Code skeleton

Borrow, hold, return late, and pass the copy on
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 32

Run 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 HoldReady and Overdue events (Observer); email/SMS are subscribers.
  • Different fine rules for children or reference books — more FinePolicy implementations.
  • 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.