# Library Management — decision log

The Phase 5 answer key. Every entry is a question an interviewer actually asks ("why is that a
separate class?") and the answer that earns the mark: **what change the seam anticipates, and what
the change would cost without it.**

Nineteen files, none over ~170 lines including javadoc and most under 50. `Library` holds the
*sequence* of a transaction at the desk and no rule that a requirement could reach.

| File | One reason to change |
|---|---|
| `Library` | the *sequence* of a transaction at the desk changes |
| `LibraryFactory` | what a standard library is made of changes |
| `Catalogue` / `InMemoryCatalogue` | how the holdings are stored changes |
| `Shelf` | how availability is derived from holdings and loans changes |
| `LoanRepository` / `InMemoryLoanRepository` | how the loans that are out are stored changes |
| `MemberRepository` / `InMemoryMemberRepository` | how the roll is stored changes |
| `Member` | what the library knows about a member changes |
| `LoanPolicy` / `FixedLoanPeriod` | how long a book may be kept changes |
| `Lateness` | how lateness is *measured* changes — i.e. never |
| `FinePolicy` / `DailyFine` | what lateness *costs* changes |
| `BorrowingRule` / `WithinBorrowingLimit` / `FinesUnderThreshold` | who may borrow changes |
| `Demo` | what we show when we run it changes |

---

## Seam 0 · `TimeSource` — the decision that makes the problem gradeable at all

**Anticipates:** nothing. It is not there for a future requirement; it is there because the present
one cannot be tested without it.

**This is the first thing to say out loud, before any seam.** Every rule in this problem is a rule
about time: the due date, the count of days late, the fine, whether a book is overdue. If the code
reads `Instant.now()`, then *"a book returned two days late costs a hundred"* is a statement that can
only be checked by waiting twenty-three days. The clock is a dependency like any other, and the only
unusual thing about it is how often people forget that.

**Read once per operation, at the top.** `Library` calls `clock.now()` once and passes the value
down. Two readings inside one transaction is how a loan gets a due date computed from a different
instant than the one recorded on it — a bug that only shows up when the two readings straddle a
boundary, which is to say in production and never in a test.

**What it is not.** One method. Not a `Clock` with zones and calendars, not a scheduler, not
something that can sleep. The library needs to know what time it is; anything more would have to be
implemented by every test double for no gain. And explicitly not `java.time.Clock`, which brings
zones and a `withZone` contract into a problem that has no time zones in it.

**The corollary worth stating in the defence:** there are no calendars anywhere. Three weeks is
`21 × 24` hours. Opening hours, weekends and bank holidays are all *out of scope by agreement*, and
the moment one of them is in scope, `LoanPolicy` is where it lands — which is the next seam but one.

## Seam 1 · `LoanRepository` — the loans live behind an abstraction, and it has four methods

**Anticipates:** every future question about the loans that are out, and every future decision about
where they live.

**The one decision to defend, if there is time for only one.** The obvious implementation is a
`Map<String, Loan>` field inside the class that runs the desk. It is one line, it is fast, and four
things go wrong with it in increasing order of seriousness:

1. Every question about loans is answered *inside* that class, so the class that lends, returns,
   fines and validates also holds four queries.
2. Those queries read the map directly, so the day loans live anywhere else, all four move.
3. The queries accumulate. "What is overdue" is the fourth; "what did this member borrow last year"
   is the fifth.
4. **There is nowhere to put a different storage decision.** A library's loan table is a database in
   every real deployment, and the change should be a constructor argument, not a rewrite of the desk.

**And the second decision, which the curveball actually measures: four methods, not seven.** The
tempting interface is a method per question — `findByMember`, `findByIsbn`, `findByCopy`,
`findOverdue`. That interface grows a method every time the loans are asked something new, and every
one of those methods has to be written again in every implementation. So the seam that was supposed to
absorb change becomes the place change is charged to.

Instead: `save`, `find`, `remove`, and **`outstanding()` — everything that is out.** Three base
operations already need exactly that (the borrowing-limit count, the availability derivation, one
member's list), so it is not a speculative method. A new question is then a filter over it, in a new
file.

**Cost without it.** Curveball 3 asks for a library-wide overdue report *and* a lending rule that
refuses on it. With this seam it is **two new files plus five lines, three of which are a row in a
list**. With the map inside the desk it is a query method and a fourth guard clause in the two
methods that must not break, and the pattern is set for the next one.

**The honest cost of the choice.** `outstanding()` is O(n) and the queries scan it. For one branch
that is free and it is the right trade while it is free. When it stops being free the answer is a
second implementation of this interface holding indexes by member and by copy — a new file and one
line in the factory. *Where a decision can be made cheaply* is what the interface buys; making it
now would be guessing.

**Why `remove` rather than a `returned` flag.** The requirement says a returned loan is finished
with, and `outstanding()` is named for the distinction. If history were ever wanted, this interface is
where it arrives, and the method that already means "still out" would not need renaming.

## Seam 2 · `Shelf` + `Catalogue` — availability is derived, never stored

**Anticipates:** the catalogue moving to a shared database, which is the first thing that moves in a
real library.

**The single source of truth this protects, and the bug it prevents.** *"Is there a copy on the
shelf?"* is a question neither the catalogue nor the loan drawer can answer alone: the catalogue knows
what the library owns, the drawer knows what has left the building, and availability is the
difference. The tempting alternative is a `borrowed` boolean on each copy, set on the way out and
cleared on the way back.

That is the same fact written down twice. The first time a return goes wrong halfway through, they
disagree, and a copy is either lost or lent to two people. Here there is exactly one fact — a loan
exists — and availability is computed from it, so the two cannot drift. **A design that stores
availability has to be asked how it stays consistent; this one cannot be inconsistent.**

**Why `Shelf` is its own class rather than a helper on the desk.** Two operations need the answer
(`availableCopies` and `borrow`), so if the desk held the derivation it would be written twice or
hidden in a private method that is really this class without a name. The two refusals that are *facts
about the shelf* — an ISBN nobody has heard of, and no free copy — are thrown here for the same
reason `SlotRack` throws them in the vending machine: neither is a decision about lending.

**Why barcodes are minted in `InMemoryCatalogue`.** A title says how many copies the library owns;
the catalogue turns that into that many identified copies, once, at construction. So a title's copy
count and its copies can never disagree — there is only one number. The format is nobody's business
but that class's, which is why no test asserts one.

**What was deliberately not built: a `Copy` class.** It would hold a barcode and an ISBN, and nothing
in this problem does anything with a copy except tell it apart from another copy. The barcode *is* the
copy. A one-field entity is a class earning nothing — the same call as the vending machine's absent
`Product`. If a copy ever acquired a condition, an acquisition date or a home branch, that is when it
becomes a class.

## Seam 3 · `LoanPolicy` and `FinePolicy` — two questions, and they are not one question

**Anticipates:** the two rules a library changes most often, and it changes them independently.

`LoanPolicy` answers *how long may this be kept*; `FinePolicy` answers *what does lateness cost*. Both
collapse into one-liners inside the methods that lend and return — `borrowedAt.plus(ofDays(21))` and
`daysLate * 50` — and that is exactly why they are worth separating: those two lines are where the
next two requirements would have landed, next to code whose meaning they change.

**The parameter choices are the interesting part, and they were not free.**

- `LoanPolicy` takes the **`Title`**, not just the ISBN, because every plausible variation is a rule
  about what kind of book it is. Curveball 1 (reference books, three days) is a decorator because of
  that; with only an ISBN it would have been an edit to the interface.
- `FinePolicy` takes the **`Title`** as well as the day count, on the same argument. Curveball 2
  (cap at the replacement cost) is *inexpressible* without it — the ceiling is a fact about the book.
  The base design has one implementation that ignores the parameter, which looks like waste until that
  requirement arrives.
- `FinePolicy` deliberately does **not** take the `Member`, even though *"children are not fined"* is
  just as plausible. That one is a much larger claim about the library, and guessing at both
  parameters would be designing for two futures instead of one. Being able to say which future you
  designed for, and why you stopped there, is the point.

**Why `Lateness` is separate from `FinePolicy`.** *"Any part of a day past the deadline counts as a
whole day"* is a **definition**: it is how the library measures lateness, it is printed on the receipt
whether or not there is a charge, and it does not change when the tariff does. "Fifty a day" is a
**price**. Had the ceiling from curveball 2 lived where the days are counted, capping the money would
have quietly capped the count — and the requirement explicitly asks for the days to stay true. Here
that is not something anybody had to remember.

**Why `Lateness` is a static utility and not an interface.** There is no second way to count days and
no requirement that suggests one. An interface with one implementation and no foreseeable second is a
seam that costs a file and earns nothing. Naming what is *not* a seam is part of the design, and this
is the one to name.

## Seam 4 · `BorrowingRule` — a list, so precedence is data

**Anticipates:** eligibility, which is the part of a library that accumulates rules without limit.

The requirement already has two rules — a borrowing limit and a fines threshold. Written inline that
is two guard clauses at the top of the method that also finds a copy and mints a loan, and the third
goes there too.

**The stated precedence becomes the order of a list**, in the factory, rather than the order two `if`
statements happen to be written in. That is the difference between a rule you can point at and a rule
somebody has to read a method to discover. Curveball 3 adds a third rule: **one new file, and one row
in the list.**

**Why it throws instead of returning a verdict.** Each refusal has its own exception carrying its own
numbers, and the contract says the first applicable one wins. A `boolean` loses which rule refused; an
`Optional<String>` loses the numbers. A `List<Refusal>` would be the right shape for a desk that
wanted to show every problem at once — and that change is to this interface and its three
implementations, not to the lending method. Being able to say what the *next* shape would be is worth
more than claiming the current one is inevitable.

**Why every rule is handed the clock's reading, whether or not it uses it.** In a library where the
loan period, the fine and the overdue status are all functions of time, a rule that cannot ask what
time it is would be the odd one out — and the alternative is that the first time-dependent rule
changes the interface. Two of the three base implementations ignore it. That is the cost, and it is
one parameter.

## Seam 5 · `LibraryFactory` — construction is a job, and it has an owner

**Anticipates:** everything about what a library *is*, as opposed to what it *does*.

**The decision worth defending: `Library` has no convenience constructor.** Every collaborator is
injected and there is deliberately no `Library(TimeSource)` that supplies its own catalogue and
lending terms. That looks like ceremony until you ask where a requirement lands. All three curveballs
are changes to what the library is made of, and **none of them opens the class that runs the desk**:

| Curveball | Where it landed | Measured cost |
|---|---|---|
| reference books go out for three days | one line in the factory + one new file | **2 lines** |
| fines cap at the replacement cost | one line in the factory + one new file | **2 lines** |
| the overdue list, and a rule to match | one row in the factory + two new files + the new contract method | **5 lines**, 2 of them declaration |

Had the catalogue and "three weeks" sat inside a `Library(…)` convenience constructor, all three would
have edited the class that validates the rules, allocates a copy and charges the fine.

**Why the three numbers live together.** `LOAN_PERIOD`, `FINE_PER_DAY_MINOR` and
`FINE_BLOCKING_MINOR` are one story: this is how this branch lends. Scattered as private constants
across three policy classes they would be three separate things to find, and nothing would say they
belong to the same decision.

**Why the parts are exposed as well as the whole.** `standardCatalogue()`, `standardMembers()` and
`standardBorrowingRules()` are public so a variation on the standard library reuses what it is not
changing instead of copying it.

## The commit discipline · ask, act, commit

Every operation in `Library` is the same three steps:

1. **Ask.** Read the clock once, then establish that everything the operation needs exists and is
   allowed — the member, the title, the rules. Nothing has changed.
2. **Act.** Work out the whole outcome — the copy, the due date, the fine — throwing if any of it is
   impossible. Still nothing has changed.
3. **Commit.** Write the loan, or free the copy and charge the member, with nothing left that can
   fail.

This is why every refusal leaves the library exactly as it was, and it is a **property of the
ordering** rather than of any try/catch. A half-finished loan rolled back would be a second copy of
the lending logic running backwards, and those go stale.

The precedence of the five refusals falls out of this ordering rather than being encoded separately:
member, then title, then the rules in their listed order, then availability. Each check is where its
knowledge lives — the member repository knows who exists, the shelf knows what is held and what is
free, the rules know who may borrow — and the order they run in *is* the requirement's order.

Note also that a loan id is minted *after* the last thing that can throw, so a refused request does
not silently consume one.

## Entity vs value · why `Member` is a class and `Loan` is a record

The distinction, and the bug that makes it more than pedantry.

A **member is an entity**: Ada is Ada whether she owes nothing or owes five hundred. Her identity is
`memberId` and nothing else, so `equals` compares only that. A **loan is a value snapshot** — a
record, equal to another only if every field matches — because a loan never changes: it is created,
and later it is gone.

**A record would have been wrong for `Member`, and wrong in a way that bites.** Generated `equals`
over all components means Ada-owing-0 and Ada-owing-500 are different objects, so
`members.remove(ada)` silently does nothing once she has been fined, and a `Set<Member>` can hold the
same person twice. Identity equality is not decoration; it is what makes a collection of entities
behave.

**Being an entity does not mean being mutable.** `Member` is immutable, and paying a fine produces a
new value with the same identity, written back through `MemberRepository.save`. A mutable entity
handed out by a repository is changed by whoever holds it with no call to `save` — which works by
accident for an in-memory map and silently loses the write against anything else. Making the write
explicit means the same code is correct against a database.

## Errors · one hierarchy, and where the line is drawn

The contract fixes the exception types because the grader must name what it expects, but the shape is
the shape a good answer would have chosen:

- **Six types, not one with six messages.** Callers act differently on them. A kiosk says "we don't
  have that book" for one and "you're at your limit" for another; the finance system watches only
  `OutstandingFinesException`. One type would force every caller to read prose.
- **A common abstract root**, so a caller that does not care can catch one thing, and
  `catch (LibraryException)` is exhaustive over the domain. Abstract because *"something went wrong at
  the library"* never happens — one of the six specific things does.
- **They carry data, not just prose.** `BorrowingLimitReachedException` knows the limit and the
  count; `OutstandingFinesException` knows the amount and the threshold. The desk needs the numbers,
  and computing them twice in two places is how the two drift apart.
- **`payFine` throws `IllegalArgumentException`, not a `LibraryException`.** This is the line worth
  defending: paying a fine you do not owe, or paying nothing, is a **caller bug**, not a refusal the
  library makes. A member being over their limit is an ordinary event in a library's day; a caller
  asking to pay negative money is a programming error, and dressing it up as a domain event would put
  it in the same `catch` block as the things a kiosk is supposed to display.
- **One exception for two situations, deliberately.** `UnknownLoanException` covers both an id that
  was never issued and one already returned. The library keeps no history, so after a return it
  genuinely cannot tell them apart — and the desk says the same thing either way. If history is ever
  kept, that is when they become two.

## Collections never escape

`catalogue()` and `loansOf()` return `List.copyOf`/`toList()` results. `InMemoryCatalogue` copies the
barcode list at accession, `InMemoryLoanRepository.outstanding()` copies, `Library` copies the rule
list in its constructor so the rules cannot be edited after the library is built. A caller can read
everything and corrupt nothing — which matters most for exactly the parts a candidate is expected to
replace.

## Time is central, and calendars are absent

There is no `LocalDate`, no time zone and no calendar anywhere: due dates and lateness are `Instant`
arithmetic. That is a **scope agreement, not an oversight**, and it is worth saying out loud, because
the real rule in a real library is *"due at closing time on the twenty-first day, and Sunday does not
count"*. That rule is a `LoanPolicy` with a calendar in it and a `Lateness` that skips closed days —
which is to say it lands in the two places already built for it, and in nothing else.

## What was deliberately *not* built

Named so the scope is explicit rather than forgotten:

- **No reservations, holds or waiting lists.** Stated out of scope, and the most-asked clarifying
  question. It is the one requirement in this domain that would *not* be cheap here: a returned copy
  going to the next member in a queue means the return path has to consult a queue, so it reaches
  `giveBack` rather than only the factory. The honest answer is that it needs a `Reservations`
  collaborator and a decision about whether a held copy counts as available — and pretending
  otherwise would be the wrong kind of confidence.
- **No renewals.** A renewal is a new loan with the same copy, so it is a method on the desk and a
  rule about how many times, not a new concept. Out of scope by agreement.
- **No loan history.** Which is why a returned loan is `remove`d. If it were wanted,
  `LoanRepository` is the one file that changes, and `outstanding()` already means the right thing.
- **No rule against holding two copies of one title.** The contract explicitly allows it. Worth
  noticing that forbidding it would be a **new `BorrowingRule`** — a new file and a row in a list —
  and not a change to how loans are stored. That is the seam demonstrating itself on a requirement
  nobody asked for.
- **No locks.** The contract states one desk at a time. `synchronized` on every method would be cargo
  cult; the right move is to name the shared mutable state — the loan drawer, the member roll, and the
  loan-id counter — and say where the boundary would go if the desk had a network interface. Note
  that the read-modify-write in `giveBack` (read the member, add the fine, save) is the operation that
  would need to be atomic, not the individual repository calls.
- **No `IdGenerator` seam.** Loan ids come from a counter in `Library`. Nothing in the requirement
  makes an id mean anything, so an interface for minting them would anticipate a change nobody has
  described. If ids ever had to be branch-prefixed or globally unique, one line moves.
- **No search.** `catalogue()` returns everything, in order, and there are four titles. Pagination and
  text search are a different problem with a different shape, and inventing them here would be
  building for a library this is not.
