LLD-004 · DESIGN PRACTICE
Library Management
Separate titles from copies, protect circulation invariants and preserve historical loan terms.
Local Java 21 reference verified: 221 checks. Learner drafts are not executed. Author self-review; independent review pending.
Existing introductory lesson · Version 1
Understand the problem
Design circulation for a library with multiple physical copies of a title. Support checkout, return, one renewal and a FIFO title-hold queue. Protect copy availability and member limits under concurrent calls. Explain how a policy update affects existing versus new loans.
Before you start
- Entity identity vs value equality
- Transactions and lock scope
- Calendar dates and immutable policies
- FIFO queue semantics
Scope and explicit decisions
- In-memory circulation for registered members and configured physical copies. No catalog search engine, fines, procurement, authentication implementation, ebook licensing or inter-library transfers.
- BookCopy has a stable copy ID and title ID. Several copies may share a title; lending the title itself would incorrectly collapse inventory.
- A Policy contains version, loan duration in whole calendar days and active-loan limit. The current policy applies at checkout; each Loan captures its immutable policy. A policy decrease does not forcibly return existing loans.
- Trusted callers supply LocalDate in the library timezone. No clock or timezone conversion occurs inside the model; deadline is a calendar date, not a UTC duration. Renewal on the due date is allowed; later renewal is rejected.
- One renewal is allowed, extending the original due date by the captured policy duration. Any waiting title hold blocks renewal. Renewal uses expectedRenewals as an optimistic version and is not automatically retried with a different version.
- Holds are FIFO by insertion and unique per member/title. Checkout consumes the head when present. Head priority applies to any copy of that title, even if several copies are free. An ineligible head blocks later members until its hold is cancelled; expiry and automatic skipping are not implemented.
- checkout requestId is globally scoped to this Library instance. Reusing it for another copy/member fails; a retry resolves the same loan identity with its current returned state.
- Returns are idempotent by loan ID. Retained loans and command results grow over time and disappear on process loss. Durable transactions and post-commit notifications are design extensions.
Worked examples
Input: c1 and c2 share title T. A borrows c1; B and C place holds for T.
Outcome: Queue [B,C]. A cannot renew while readers wait; C cannot bypass B even for free c2.
This chapter deliberately uses strict title-head priority. A more permissive allocation rule must be designed explicitly.
Input: Loan under v1=14 days begins January 1. Policy changes to v2=7 days. Renew old loan on January 15.
Outcome: Old loan extends to January 29. A new January 1 loan under v2 is due January 8.
The version used by a record determines its historical term.
Your design attempt
Each language has a separate draft across all three levels. Saving is device-only and does not sync to your account. Anyone using this browser profile can load it. Loading is always explicit.
Java 21 reference code is provided for local study. Use Run program in the editor to execute your code and test driver. Program runs show output; they do not grade your design.
Run program · Java 21
No unsaved changes · 208/20000
Self-review
Checks apply to this visit. Self-review is not a passed submission or verified mastery.
Try before revealing
Hint 1
A title describes a book; which identity describes the thing being lent?
Hint 2
Which facts must checkout update atomically besides copy availability?
Hint 3
Keep old loan identity and policy so retries and upgrades cannot rewrite history.
Requirements and invariants
R1 — A physical copy has at most one active loan, and member active counts match those loans. checkout protects I1.
R2 — Title holds preserve FIFO priority without duplicate membership. placeHold protects I2.
R3 — A policy change cannot silently rewrite existing loan terms. changePolicy protects I3.
R4 — Repeated return of an old loan cannot free a copy now checked out to another member. returnCopy protects I1.
I1: Each nonreturned loan corresponds to exactly one activeCopies entry; each member count equals their active loans.
I2: Each title queue contains each member at most once; only its head may claim a copy while the queue is nonempty.
I3: A Loan retains its captured Policy; successful renewal extends its due date using that Policy, at most once.
Derive the design
Model copy ownership before adding repositories
Begin with a list of loans and scan for the active loan of a copy or member. It is correct under single-threaded access but costs O(L) per check. The reference adds activeCopies and per-member counts for expected O(1) access while keeping the loan log as the source for diagnostic invariant checks. These indexes are materialized state and must be maintained atomically, not treated as independent truths.
Alternative: A relational repository can enforce unique active copy ownership and use a transaction for member capacity and loan creation. A generic CRUD repository is insufficient if checkout performs several uncoordinated calls. Expose a domain operation or explicit unit of work with its guarantees.
Ask about copy and hold granularity
Do readers reserve a title or one physical item? Can two copies be checked out independently? Answer those questions before drawing class inheritance. The chosen model has many copies per title and one active loan per copy.
Write the invariant across all records
The service owns a loan, active copy mapping and member count. Those facts must agree. Keeping a cached count is useful only if checkout and return update it under the same atomic boundary as the authoritative loan.
Put validation before irreversible queue changes
Read hold head, member count and copy state first. Compute LocalDate.plusDays before modifying maps. Only after creating a valid loan should the hold be consumed, so arithmetic or eligibility failure cannot lose priority.
Preserve identity through repeat returns
A return is attached to a loan ID, not merely a copy ID. If a stale return arrives after that copy was borrowed again, the old returned Loan resolves as a no-op and must not clear the new active mapping.
Responsibilities and ownership
BookCopy
Owns: Unique physical identity and bibliographic title reference
Methods: Validated immutable accessors
Does not own title-wide reservations or loan policy.
Loan
Owns: Member/copy identities, due date, captured policy, renewal version, returned flag
Methods: Immutable snapshot accessors
Library replaces snapshots; callers cannot edit due dates or return status.
Library
Owns: Active copy mapping, member counts, holds and request results
Methods: checkout, returnCopy, renew, placeHold, cancelHold
One local atomic boundary coordinates cross-record invariants.
Policy
Owns: Versioned term and limit values
Methods: Validated immutable constructor
Policy replacement affects new loans; historical meaning remains attached to old records.
A repository adapter must preserve the same cross-record contract; CRUD calls alone do not imply a transaction.
- Desk command + request ID
↓ Validate membership and identity
Library atomic boundary↓ Commit matching records and consume hold
Loan / copy / member count↓ Return stable loan identity
Immutable loan snapshot
One guarded renewal extends the active due date. A repeated return does not touch a later loan on the reused copy.
- Available copy
↓ Eligible checkout; respect title hold head
Active loan with captured policy↓ Return by loan ID
Returned historical loan
Data model, schema and relationships
A bibliographic title is not a physical copy. Members borrow copies, while FIFO holds request a title; allocation connects the next eligible member to one copy.
Implementation boundary: Title/copy/member/loan/hold records mirror reference concepts. A database requires transactionally enforced member limits and copy ownership.
These are proposed logical schemas with field contracts, not executable database migrations. PK identifies a row, FK references an existing parent, UK is a uniqueness rule. A composite PK identifies the combination. Nullable references are marked with ?. A foreign key protects identity; cross-row limits, timing and state transitions also require the transaction rules below.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Entity definitions and field contracts
title
Bibliographic identity.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Title identity |
isbn | text · UK | Edition identifier |
name | text · VALUE | Display title |
Code mapping: Title record.
- ISBN may be optional/nonunique in a broader catalog; declare edition rules.
copy
One physical lendable item.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Barcode identity |
title_id | uuid · FK | Bibliographic parent · title.id |
state | enum · VALUE | Available reserved or loaned |
Code mapping: Copy record.
- At most one active loan/reservation per copy.
member
Borrower eligibility.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Member identity |
active | boolean · VALUE | Membership eligibility |
Code mapping: Member state.
- Borrowing limit spans all active member loans.
loan
Historical copy checkout.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Loan identity |
copy_id | uuid · FK | Borrowed item · copy.id |
member_id | uuid · FK | Borrower · member.id |
due_at | timestamp · VALUE | Due instant |
policy_version | text · VALUE | Original loan rules |
closed_at | timestamp? · VALUE | Null while active; terminal return evidence otherwise |
Code mapping: Loan record.
- Partial unique active copy_id; return must correlate current loan identity.
hold
FIFO title request.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Hold identity |
title_id | uuid · FK | Requested title · title.id |
member_id | uuid · FK | Requester · member.id |
sequence | bigint · VALUE | FIFO order |
status | enum · VALUE | ACTIVE allocated fulfilled or cancelled queue state |
Code mapping: Hold queue.
- Unique active (title_id,member_id); stable sequence orders eligible holds.
Relationship cardinalities and their reasons
| Parent → child | Multiplicity | FK / why |
|---|---|---|
| title → copy has physical copies | 1 parent per child; 0..* children per parent | copy.title_id → title.idMany copies share one edition; a checkout consumes one copy. |
| copy → loan has loan history | 1 parent per child; 0..* children per parent | loan.copy_id → copy.idOnly one historical loan may remain active. |
| member → loan borrows | 1 parent per child; 0..* children per parent | loan.member_id → member.idMember limit needs a shared atomic owner across concurrent different-copy checkouts. |
| title → hold has requests | 1 parent per child; 0..* children per parent | hold.title_id → title.idFIFO is per title, not per arbitrary copy. |
| member → hold requests | 1 parent per child; 0..* children per parent | hold.member_id → member.idA member may wait on many titles under policy limits. |
Transaction boundaries
- checkout: lock member eligibility/active count and selected copy/hold order, then insert loan and update copy atomically.
- return: validate active loan identity, close once and allocate availability to the next eligible FIFO hold.
Indexes and uniqueness
- Partial unique active loan(copy_id); index active loan(member_id) for limits.
- Index active hold(title_id,sequence) for FIFO; unique active title/member request.
Schema traps and failure cases
- Checking member limit separately from inserting loans allows concurrent different-copy oversubscription.
- A stale return cannot release a copy under a newer loan.
Trace the records through one operation
- Title T has copies C1,C2; member M requests T.
- Checkout C1 creates L1 under current policy and member limit.
- Return L1 closes it once and advances the FIFO allocation without changing title identity.
Method contracts
checkout(copyId, memberId, requestId, today) → Loan
Validate identity, retry fingerprint, copy availability, member limit and hold priority. Compute due date before mutation; commit loan, count, copy ownership and dedupe result together.
returnCopy(loanId) → Loan
If already returned, return that snapshot without touching current copy ownership. Otherwise mark returned and decrement the associated member count atomically.
renew(loanId, expectedRenewals, today) → Loan
Reject returned/overdue/already-renewed loans, stale expectedRenewals and any waiting hold. Extend original due date under captured policy, then increment renewal count.
placeHold(titleId, memberId), cancelHold(titleId, memberId)
Validate configured title and registered member. Placement is duplicate-safe; cancellation removes the member if present. Both operate under the same aggregate monitor.
changePolicy(Policy)
Replace the current immutable policy for future checkout; no retroactive update of active or historical loans.
What, when, why and how
Each concept is attached to the requirement it protects, its implementation, an alternative and a test.
Entity identity versus title data
- what
- A physical copy is a separate identity from the shared bibliographic title.
- when
- The library owns multiple copies of one book.
- why
- Using ISBN/title as the loan key would either block all copies or allow the same physical item to be lent twice.
- how
- BookCopy stores copyId and titleId. Active ownership is keyed by copy ID; hold queues are keyed by title ID because a reader generally accepts any copy.
Code: BookCopy, activeCopies, placeHold · Invariants: I1, I2
Alternatives, cost and failure example
Alternative: If holding a specific rare copy is required, introduce copy-scoped holds deliberately; do not overload title queue semantics.
Cost: Two identity spaces require explicit joins/lookups, but preserve correct cardinality.
Counterexample: Two copies of the same title share one boolean available flag; returning one makes both appear available.
Test: Configure c1/c2 with one title; checkout c1 and separately exercise c2 under member/hold constraints.
Aggregate atomicity and derived indexes
- what
- Treat loan creation, copy ownership, member count and request result as one change.
- when
- Concurrent circulation commands can interleave.
- why
- A safe copy map alone cannot enforce a member borrowing two different copies simultaneously against a limit of one.
- how
- synchronized checkout validates all conditions and writes all maps under one monitor; return and renewal use the same owner. invariantHolds recomputes active counts from loans.
Code: checkout, returnCopy, invariantHolds · Invariants: I1
Alternatives, cost and failure example
Alternative: For several processes use database constraints plus a transaction protecting both copy and member admission, with stable lock ordering or serializable conflict handling.
Cost: One monitor limits parallelism; indexes add maintenance complexity but speed normal lookup.
Counterexample: Two desks independently read member count zero, then each creates a loan on a different copy.
Test: Race checkout and verify one-copy ownership; repeated circulation recomputes and compares all counts after every operation.
FIFO reservation semantics
- what
- A title queue orders who may claim the next eligible copy.
- when
- Returns and new checkouts compete with waiting readers.
- why
- Without an explicit priority rule a newly arriving borrower can bypass a long-standing hold.
- how
- placeHold deduplicates member entries; checkout rejects non-head borrowers and removes the head only after successful loan creation. Renewal is blocked by any pending hold.
Code: placeHold, checkout, renew · Invariants: I2
Alternatives, cost and failure example
Alternative: Automatically skip ineligible/expired holds only with a documented deadline and notification policy; this simple reference requires explicit cancellation.
Cost: Queue contains/remove is O(H). Strict head priority can reduce utilization when the head cannot currently borrow.
Counterexample: Pop the head before checking member limit; a failed checkout silently loses that reader’s reservation.
Test: Repeat the same hold, attempt non-head checkout, fail renewal with waiting holds, then verify FIFO consumption.
Immutable policy snapshots and optimistic renewal
- what
- Retain the rule that created a loan, and reject a renewal based on stale state.
- when
- Policy changes or two renewal requests occur while a loan is active.
- why
- Current configuration must not silently shorten old terms; duplicate renewal should not extend the due date twice.
- how
- Loan stores Policy. renew requires expectedRenewals, allows at most one extension and applies the captured duration to the existing due date.
Code: Policy, renew, changePolicy · Invariants: I3
Alternatives, cost and failure example
Alternative: If renewals intentionally use current policy, version that business decision and show its impact on existing users instead of treating it as an implementation detail.
Cost: Snapshots retain old policy versions and make data retention/serialization contracts important.
Counterexample: Change 14-day policy to 7 days and accidentally rewrite the existing due date while calculating a renewal.
Test: Verify old v1 renewal adds 14 days, new checkout uses v2 seven days, and stale renewal version is rejected.
Read and run the Java reference
Reveal complete Solution.java
Java 21. Save as Solution.java; compile with javac --release 21. The repository also contains Test.java and the lld:java verification command. This reference implements the local contract above.
checkout keeps request identity separate from availability
An accepted request resolves before a current availability decision, so its retry still refers to the original loan. The fingerprint is copy+member; today is treated as execution context rather than a new business command.
returnCopy checks old terminal state first
A duplicate return exits before activeCopies.remove. That ordering prevents a stale return from removing another borrower’s active loan after copy reuse.
renew extends an existing promise
Use due.plusDays(capturedPolicy.days), not today plus days. This prevents an early renewal from shortening the existing period and makes terms deterministic.
holdQueue returns an immutable snapshot
External callers can inspect order without mutating the queue. Changes go through placeHold/cancelHold, which preserve member validation and uniqueness.
Complexity and limits
- Checkout uses expected O(1) copy/member/request maps and O(1) queue-head access. Due-date arithmetic is constant for bounded LocalDate operations.
- placeHold/cancelHold scan H title reservations for duplicate/removal, and current title validation scans C configured copies. A title index can remove that O(C) scan after measurement.
- invariantHolds scans L retained loans and M members: O(L+M). It is a diagnostic/test check, not required in the hot path.
- Space O(C+M+L+H+R) for copies, members, retained loans, holds and request records. Retention is intentionally unbounded over process lifetime; durability is not implemented.
Walk through the actual reference
These walkthrough fixtures are emitted only after the Java harness verifies their operations and expected state. Concurrency tests additionally exercise competing callers.
Copy identity and hold priority
Step 1 of 3: A checks out c1
Loan L1 due January 15; c1 unavailable
Invariant: One active loan per copy
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| A checks out c1 | Loan L1 due January 15; c1 unavailable | One active loan per copy |
| B then C reserve title | Queue [B,C]; A renewal rejected | Waiting readers have priority |
| A returns, B checks out c1 | B now owns active copy; queue [C] | Old loan return cannot release new loan |
Policy cutover preserves loan terms
Step 1 of 3: Change v1 14 days to v2 7 days
Existing loan still refers to v1
Invariant: Historical policy immutable
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Change v1 14 days to v2 7 days | Existing loan still refers to v1 | Historical policy immutable |
| Renew B at original due date | Due extends to January 29 under v1 | Renewal uses captured term |
| Create C new loan | Due January 8 under v2 | New loans use new policy |
Mistakes and counterexamples
Treat a title as the lendable object
Failing case: c1 and c2 share title T; lending c1 makes c2 disappear.
Repair: Separate title metadata from physical copy identity.
Return by copy ID alone
Failing case: An old return event arrives after the same copy has been loaned to B.
Repair: Return by loan ID and check its terminal state before touching current ownership.
Consume a hold before checkout succeeds
Failing case: Head member is at the borrowing limit.
Repair: Validate first; pop the queue only with committed loan creation.
Renew with mutable global policy
Failing case: A new seven-day policy unexpectedly changes a fourteen-day loan.
Repair: Capture versioned terms and document whether renewal retains or switches them.
Engineer → Senior → Staff
Model circulation using the correct identities.
Executed: checkout, return, due dates, member limits and basic title holds.
Change exercise
Add two copies of one title and demonstrate independent availability without bypassing the member cap.
Worked answer guidance
Key physical ownership by copyId. Keep titleId only for shared metadata and title-wide holds. Show the active loan/copy relationship directly in tests.
Interview follow-ups
Why is BookCopy an entity despite being represented as a record?
Entity is a domain concept based on identity, not a Java keyword. The record carries a stable copy ID; immutable representation does not erase identity.
Why use LocalDate rather than milliseconds for due dates?
The rule is expressed in library calendar days. Milliseconds introduce timezone/DST ambiguity; convert at the trusted boundary using the library zone.
Provenance and scope of evidence
Original teaching material. The local contract is implemented by Solution.java and tested by Test.java on Java 21. Distributed persistence, real provider/device adapters and deployment are not claimed.
Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.