Low-Level Design · lesson 7 of 18
Beginner
4Intermediate
10- 5 Design a Parking Lot: Allocating and Freeing Spots
- 6 Design an Elevator System: Scheduling Requests
- 7 Design a Library Management System: Borrowing and Returning Books
- 8 Design an Expense-Splitting App: Balances and Minimal Settlements
- 9 Design a URL Shortener: Sequential Codes vs Real Hash Collisions
- 10 Wrapping Objects: Adapter, Facade, and Proxy in Python
- 11 The Composite Pattern in Python: Sizing a File Tree Without isinstance
- 12 The State Pattern in Python: Refactoring a Vending Machine Into State Classes
- 13 Undo and Redo in Python: the Memento and Command Patterns Compared
- 14 Chain of Responsibility in Python: Expense Approvals Without an if/elif Ladder
Advanced
4- 15 Concurrency in LLD: A Real Race Condition in the Parking Lot, Then the Fix
- 16 The Observer Pattern in Practice: A Stock Ticker Notifying Multiple Displays
- 17 Design Patterns That Are Just Language Features: Strategy, Command, Iterator
- 18 Abstract Factory and Prototype in Python: Matching Families and Cloning Objects
Also part of Low-Level Design
12- + SOLID Principles in Java from Java
- + Composition vs Inheritance in Java from Java
- + Design Patterns in Java from Java
- + Python Design Patterns from Python
- + Design Patterns in TypeScript from TypeScript
- + JavaScript Design Patterns from JavaScript
- + Design Patterns in C# from C#
- + Design Patterns in C++ from C++
- + Design Patterns in Go from Go
- + Design Patterns in Rust from Rust
- + Abstract Classes in Java from Java
- + Sealed Classes and Interfaces in Java from Java
Design a Library Management System: Borrowing and Returning Books
Real date arithmetic decides whether a returned book is late and what the fine is — not a string comparison, and not a guess.
By SkillByExample · Published
A library system’s actual complexity isn’t the catalog — it’s computing “late” correctly.
Borrowing, returning, and fines
from datetime import date, timedelta
class Book:
def __init__(self, isbn, title, copies):
self.isbn = isbn
self.title = title
self.total_copies = copies
self.available = copies
class Library:
LOAN_DAYS = 14
FINE_PER_DAY = 10
def __init__(self):
self.catalog = {}
self.loans = {} # (member_id, isbn) -> due_date
def add_book(self, book):
self.catalog[book.isbn] = book
def borrow(self, member_id, isbn, today):
book = self.catalog.get(isbn)
if book is None:
print(f"No such book: {isbn}")
return
if book.available <= 0:
print(f"'{book.title}' has no copies available")
return
book.available -= 1
due_date = today + timedelta(days=self.LOAN_DAYS)
self.loans[(member_id, isbn)] = due_date
print(f"{member_id} borrowed '{book.title}', due {due_date}")
def return_book(self, member_id, isbn, today):
key = (member_id, isbn)
due_date = self.loans.pop(key, None)
if due_date is None:
print(f"{member_id} has no active loan for {isbn}")
return
book = self.catalog[isbn]
book.available += 1
if today > due_date:
days_late = (today - due_date).days
fine = days_late * self.FINE_PER_DAY
print(f"{member_id} returned '{book.title}' {days_late} day(s) late. Fine: {fine}")
else:
print(f"{member_id} returned '{book.title}' on time")
lib = Library()
lib.add_book(Book("978-1", "Clean Code", 1))
lib.borrow("alice", "978-1", date(2026, 1, 1))
lib.borrow("bob", "978-1", date(2026, 1, 2))
lib.return_book("alice", "978-1", date(2026, 1, 20))
lib.borrow("bob", "978-1", date(2026, 1, 21))
$ python library.py
alice borrowed 'Clean Code', due 2026-01-15
'Clean Code' has no copies available
alice returned 'Clean Code' 5 day(s) late. Fine: 50
bob borrowed 'Clean Code', due 2026-02-04
Bob’s first borrow attempt fails — the library’s only copy is with Alice. Once Alice returns
it 5 days late (2026-01-20 vs. the 2026-01-15 due date), Bob’s second attempt succeeds,
and his own due date is computed fresh from his borrow date, not Alice’s.
Practice
1. Borrow the same title with two available copies.
from datetime import date, timedelta
class Book:
def __init__(self, isbn, title, copies):
self.isbn = isbn
self.title = title
self.available = copies
class Library:
LOAN_DAYS = 14
def __init__(self):
self.catalog = {}
self.loans = {}
def add_book(self, book):
self.catalog[book.isbn] = book
def borrow(self, member_id, isbn, today):
book = self.catalog.get(isbn)
if book.available <= 0:
print(f"'{book.title}' has no copies available")
return
book.available -= 1
due_date = today + timedelta(days=self.LOAN_DAYS)
self.loans[(member_id, isbn)] = due_date
print(f"{member_id} borrowed '{book.title}', due {due_date}. Copies left: {book.available}")
lib = Library()
lib.add_book(Book("978-2", "The Pragmatic Programmer", 2))
lib.borrow("alice", "978-2", date(2026, 1, 1))
lib.borrow("bob", "978-2", date(2026, 1, 1))
lib.borrow("carol", "978-2", date(2026, 1, 1))
$ python two_copies.py
alice borrowed 'The Pragmatic Programmer', due 2026-01-15. Copies left: 1
bob borrowed 'The Pragmatic Programmer', due 2026-01-15. Copies left: 0
'The Pragmatic Programmer' has no copies available
Two copies means two successful borrows before the third is refused — available reaching 0
is what triggers the rejection, not a hardcoded “one copy” assumption anywhere in borrow().
2. Confirm returning a book with no active loan is rejected cleanly.
from datetime import date
class Library:
def __init__(self):
self.loans = {}
def return_book(self, member_id, isbn, today):
key = (member_id, isbn)
due_date = self.loans.pop(key, None)
if due_date is None:
print(f"{member_id} has no active loan for {isbn}")
return
lib = Library()
lib.return_book("dave", "978-1", date(2026, 1, 1))
$ python bad_return.py
dave has no active loan for 978-1
dict.pop(key, None) is what makes this safe — it returns None for a missing key instead
of raising a KeyError, so the rejection is a normal, expected code path rather than a crash.
3. Confirm an on-time return charges no fine.
from datetime import date, timedelta
class Book:
def __init__(self, isbn, title, copies):
self.isbn = isbn
self.title = title
self.available = copies
class Library:
LOAN_DAYS = 14
FINE_PER_DAY = 10
def __init__(self):
self.catalog = {}
self.loans = {}
def add_book(self, book):
self.catalog[book.isbn] = book
def borrow(self, member_id, isbn, today):
book = self.catalog[isbn]
book.available -= 1
due_date = today + timedelta(days=self.LOAN_DAYS)
self.loans[(member_id, isbn)] = due_date
print(f"{member_id} borrowed '{book.title}', due {due_date}")
def return_book(self, member_id, isbn, today):
due_date = self.loans.pop((member_id, isbn))
book = self.catalog[isbn]
book.available += 1
if today > due_date:
days_late = (today - due_date).days
print(f"{member_id} returned '{book.title}' {days_late} day(s) late. Fine: {days_late * self.FINE_PER_DAY}")
else:
print(f"{member_id} returned '{book.title}' on time")
lib = Library()
lib.add_book(Book("978-3", "Refactoring", 1))
lib.borrow("erin", "978-3", date(2026, 1, 1))
lib.return_book("erin", "978-3", date(2026, 1, 10))
$ python on_time.py
erin borrowed 'Refactoring', due 2026-01-15
erin returned 'Refactoring' on time
2026-01-10 is before the 2026-01-15 due date, so today > due_date is False and the
fine branch never runs.
Next: an expense-splitting app that has to compute the minimum number of payments to settle a group’s balances.