Start with a concrete interaction: Mira borrows a physical copy of a book, another member tries to borrow that same copy, and Mira eventually returns it. The design must make the second request fail without corrupting the first loan. A diagram alone cannot demonstrate that property. We will build a small executable model and make its constraints observable through tests.
Clarify the scope before naming classes
This library already has a catalog and registered members. It lends physical copies, allows a configurable number of simultaneous loans per member, and grants fourteen calendar days per loan. Borrowing on October 2 makes the due date October 16. Returning on the due date is on time; returning the following day is overdue by one day. Dates are supplied by the caller so tests never depend on today's clock.
We deliberately omit reservations, fines, payment, renewal, and catalog editing from the initial implementation. Each requires a separate policy decision. For example, allowing renewal while another member is waiting changes a lending rule, whereas searching by title changes a read interface. Mention those boundaries during the interview and ask which feature the interviewer wants next. Do not silently implement imagined requirements and spend the session defending them.
Choose identities and ownership
A book describes a title; a copy identifies the physical object. Two copies of the same book may be borrowed simultaneously. A member has an identity and an active flag. A loan records which member holds which copy, its start date, its due date, and its optional return date. Keeping completed loans gives an audit trail and lets overdue calculations remain stable after return.
The library service owns the transition rules. Its records are frozen dataclasses, so the object returned to a caller cannot be casually modified to pretend a loan was returned. Python's dataclass documentation explains generated methods and the limits of frozen=True: it is a practical API guard, not a security boundary against hostile code. The service's underscore-prefixed dictionaries are internal implementation details.
Availability is derived from active loans rather than stored as another boolean. A separate copy status and active-loan record could disagree after a failed update. In this small model, scanning active loans is easy to explain and keeps one authoritative fact. If the catalog grows, maintain an indexed active-loan lookup with the same transition boundary rather than exposing writable status flags.
Write invariants before operations
Every copy must reference a known book, and all supplied identities must be unique within their entity type. Loan limits must be positive. A borrowing member must exist and be active; the copy must exist and have no active loan. The member's active loan count must remain below the limit. A return must reference a known active loan, come from its borrower, and occur no earlier than borrowing.
Validate everything before changing the loan dictionary or incrementing the next identifier. This gives rejection a simple meaning: state is unchanged. Retrying a successful return is rejected here, which is an explicit contract rather than an accidental error. An HTTP application could instead return the previously completed result for the same idempotency key, but that would need an additional request identity and persistence rule.
Runnable implementation and tests
Save the entire block as library_system.py and run python3 library_system.py. It uses only the standard library and works on Python 3.9 or later. The lock covers the complete check-and-write sequence. Python documents lock operations in threading; do not assume the interpreter makes a multi-step business operation atomic.
from dataclasses import dataclass, replace
from datetime import date, timedelta
from threading import RLock
import unittest
@dataclass(frozen=True)
class Book:
id: str
title: str
@dataclass(frozen=True)
class Copy:
id: str
book_id: str
@dataclass(frozen=True)
class Member:
id: str
active: bool = True
@dataclass(frozen=True)
class Loan:
id: int
member_id: str
copy_id: str
borrowed: date
due: date
returned: date = None
def overdue_days(self, today):
end = self.returned if self.returned is not None else today
return max(0, (end - self.due).days)
def unique(items):
result = {}
for item in items:
if not item.id or item.id in result:
raise ValueError("empty or duplicate identity")
result[item.id] = item
return result
class Library:
def __init__(self, books, copies, members, limit=2):
if limit < 1:
raise ValueError("limit must be positive")
self._books = unique(books)
self._copies = unique(copies)
self._members = unique(members)
if any(c.book_id not in self._books for c in self._copies.values()):
raise ValueError("unknown book")
self._limit = limit
self._loans = {}
self._next = 1
self._lock = RLock()
def borrow(self, member_id, copy_id, on):
with self._lock:
member = self._members.get(member_id)
if member is None or not member.active:
raise ValueError("member cannot borrow")
if copy_id not in self._copies:
raise ValueError("unknown copy")
active = [l for l in self._loans.values() if l.returned is None]
if any(l.copy_id == copy_id for l in active):
raise ValueError("copy already borrowed")
if sum(l.member_id == member_id for l in active) >= self._limit:
raise ValueError("loan limit reached")
loan = Loan(self._next, member_id, copy_id, on,
on + timedelta(days=14))
self._loans[loan.id] = loan
self._next += 1
return loan
def return_loan(self, member_id, loan_id, on):
with self._lock:
loan = self._loans.get(loan_id)
if loan is None or loan.member_id != member_id:
raise ValueError("unknown loan or wrong borrower")
if loan.returned is not None or on < loan.borrowed:
raise ValueError("invalid return")
completed = replace(loan, returned=on)
self._loans[loan_id] = completed
return completed
class LibraryTests(unittest.TestCase):
def setUp(self):
self.day = date(2026, 10, 2)
self.lib = Library([Book("b", "Concurrency")],
[Copy("c1", "b"), Copy("c2", "b")],
[Member("m"), Member("n"), Member("x", False)],
limit=1)
def test_lifecycle(self):
loan = self.lib.borrow("m", "c1", self.day)
self.assertEqual(loan.due, date(2026, 10, 16))
self.assertEqual(loan.overdue_days(loan.due), 0)
done = self.lib.return_loan("m", loan.id, date(2026, 10, 18))
self.assertEqual(done.overdue_days(date(2026, 11, 1)), 2)
self.assertEqual(self.lib.borrow("n", "c1", self.day).id, 2)
def test_conflict_and_limit_leave_state_usable(self):
loan = self.lib.borrow("m", "c1", self.day)
for member, copy in [("n", "c1"), ("m", "c2")]:
with self.assertRaises(ValueError):
self.lib.borrow(member, copy, self.day)
self.lib.return_loan("m", loan.id, self.day)
self.assertEqual(self.lib.borrow("m", "c2", self.day).id, 2)
def test_distinct_copies_can_be_lent(self):
self.lib.borrow("m", "c1", self.day)
self.lib.borrow("n", "c2", self.day)
def test_bad_borrowers_and_copy(self):
for member, copy in [("missing", "c1"), ("x", "c1"), ("m", "bad")]:
with self.assertRaises(ValueError):
self.lib.borrow(member, copy, self.day)
self.assertEqual(self.lib.borrow("m", "c1", self.day).id, 1)
def test_invalid_returns(self):
loan = self.lib.borrow("m", "c1", self.day)
for member, key, on in [("n", loan.id, self.day),
("m", 999, self.day),
("m", loan.id, self.day - timedelta(days=1))]:
with self.assertRaises(ValueError):
self.lib.return_loan(member, key, on)
self.lib.return_loan("m", loan.id, self.day)
with self.assertRaises(ValueError):
self.lib.return_loan("m", loan.id, self.day)
def test_invalid_catalog(self):
for books, copies, members, limit in [
([Book("b", "A"), Book("b", "B")], [], [], 1),
([], [Copy("c", "missing")], [], 1),
([], [], [], 0),
([], [], [Member("")], 1),
]:
with self.assertRaises(ValueError):
Library(books, copies, members, limit)
if __name__ == "__main__":
unittest.main()
Walk through a failed request
Suppose Mira already holds one copy and her limit is one. Borrowing a second copy passes member validation and copy existence, then fails the count check. No identifier has been consumed and no loan has been created. After she returns the first copy, the second request succeeds. The test checks that success, because merely asserting an exception could miss a rejection that accidentally changed state.
The returned loan is a snapshot. An earlier reference still describes the earlier state after a return; callers should use the newly returned record when they need completion details. This makes state transitions visible and avoids shared mutable loan objects. A production API would usually serialize snapshots and load current state explicitly on subsequent requests.
Discuss persistence and growth
Borrowing scans loan history, so its work grows with the number of recorded loans. Returning uses a dictionary lookup. That is acceptable for an interview-sized model, but long-lived systems should index active loans by copy and member. A database implementation can enforce one active loan per copy with a uniqueness rule and perform the count decision within appropriate transaction isolation.
The lock coordinates threads sharing this service instance. It cannot coordinate separate processes or servers, and it provides no crash durability. Explain that limitation before claiming production readiness. Keep storage transactions behind a service boundary while preserving these same rejection tests. Reservations should introduce a waiting policy only after its ordering, expiry, and relationship with borrowing have been specified.
When discussing dates, separate a business day from an instant. This example uses calendar dates because the policy grants whole days. If branches operate across time zones, define which branch owns the due date and when the day ends before adding timestamps. A precise policy is more useful than silently mixing local dates with UTC instants.
Next practice
Change the member limit, add an on-time return assertion, and design renewal without allowing a caller to edit due dates directly. Review OOP, database fundamentals, and databases and SQL. Practice persistence constraints in the SQL playground, then study system design fundamentals.
Continue in the SDE preparation track with SQL interview challenges, core CS interview answers, parking lot design.