Java Guild course · The Rebuild · ADRs

Reference · docs/adr/

The Architecture Decisions

Eight records, each committed alongside the change it justifies. Context, decision, consequences — including the costs, which is the half most candidates leave out.

Why these exist

An assessor asking “why did you do it this way?” is asking whether you knew there was a choice. A decision record answers that in writing, at the moment it was taken, and its Consequences section is where you name the price you paid. Two of these record deliberate deviations from the brief.

1. Pin behaviour with a golden master before changing anything

Date: 2026-09-02

Status

Accepted

Context

The supplied project has no tests at all — mvn test reports No tests to run. The work ahead touches every class in it: the exception types, the persistence format, and the whole menu loop.

Unit tests alone are not enough to start with. Writing them requires deciding what each unit should do, and those decisions are exactly what the refactor is going to change. I need something that tells me when I have changed behaviour I did not mean to change, before I have any opinion about what the behaviour ought to be.

Decision

Drive the real main() with scripted keystrokes, capture everything the user would see, and approve that transcript as the baseline.

The approved file records current behaviour, defects included. Where the application crashes, the crash is captured in the transcript rather than failing the test. It asserts what the program does, not what it should do.

ApprovalTests is used at test scope, with QuietReporter so a failing run writes a .received.txt file instead of trying to launch a diff tool on the reviewer's machine.

Consequences


2. Domain failures are checked exceptions, not return codes

Date: 2026-09-02

Status

Accepted

Context

The supplied Library signals every failure by throwing java.io.IOException from borrowBook and returnBook, neither of which performs any I/O. It also returns boolean from both. The repository ships AlreadyBorrowedException, BookNotFoundException and NotBorrowedException — named after exactly the three cases the Intermediate tier is asked to handle — and nothing references them.

IOException is wrong twice over. It describes a mechanism that is not involved, and it is a single type for three distinct situations, so the console cannot tell the user which one happened. The golden master records the result: asking to borrow a book that is already on loan reports Book not found for a book the library is holding.

Decision

Use the three supplied exceptions, under a new LibraryException supertype.

They stay checked. The usual argument against checked exceptions is that callers cannot do anything useful with them, but here the caller has a real recovery: print the message and carry on serving the user. Making them checked puts that recovery in the compiler's hands rather than in a reviewer's.

The supertype exists so the console can catch one type to report any domain failure uniformly, while a caller that needs to distinguish the cases still can.

Consequences


3. borrowBook and returnBook return void, not boolean

Date: 2026-09-02

Status

Accepted. Deviates from the brief.

Context

The brief specifies:

boolean borrowBook(String title)
boolean returnBook(String title)

Once ADR 0002 is in force, every failure leaves these methods by way of an exception. The only path that reaches a return statement is the successful one, so both methods can only ever return true.

A boolean no caller can act on is worse than no return value. It invites a reader to write if (library.borrowBook(title)) and to believe the else branch is reachable, when the compiler has already guaranteed it is not.

Decision

Return void. Success is the absence of an exception.

Book.borrowBook and Book.returnBook keep their booleans: at that level the answer is genuinely two-valued, and Library uses it to decide which exception to raise. The rule is that Book reports and Library decides how to escalate.

Consequences


4. searchBook returns Optional, not a nullable Book

Date: 2026-09-02

Status

Accepted. Deviates from the brief.

Context

The brief specifies Book searchBook(String title), and the supplied implementation returns null when nothing matches. Its own Javadoc says so: "@return the book if found, otherwise null".

Every caller therefore has to remember a check the compiler cannot enforce. The supplied Main does remember it. Library.borrowBook and Library.returnBook remember it too, in duplicated book == null branches. Nothing stops the next caller forgetting.

Decision

Return Optional<Book>.

Absence becomes part of the type, so the compiler asks the question at every call site instead of trusting each author to ask it.

Consequences


5. Storage failures are LibraryExceptions, not IOExceptions

Date: 2026-09-02

Status

Accepted

Context

saveLibraryState and loadLibraryState genuinely do perform I/O, so IOException is not the category error it was on borrowBook. But it leaves the console with two unrelated failure types to handle — LibraryException for a domain rule, IOException for storage — for what is, from the user's side, the same event: something went wrong and there is a sentence explaining it.

It also leaks the mechanism. A caller that catches IOException is coupled to the fact that state happens to live in a file today. ADR 0006 replaces that file with an interface precisely so it can be something else tomorrow.

Decision

Wrap storage failures in LibraryStorageException, a subclass of LibraryException, preserving the original as its cause.

Consequences


6. Persistence lives behind a LibraryRepository interface

Date: 2026-09-02

Status

Accepted

Context

The supplied Library was two things at once: a collection of books, and a flat-file format. addBook and borrowBook sat in the same class as BufferedWriter, URLEncoder and a field separator.

That gives the class two reasons to change — a new library rule, or a new storage format — and it makes the domain untestable without touching a disk.

The brief's Advanced tier asks for state to be kept in "a portable file-based database", and the supplied pom.xml already declares sqlite-jdbc as a dependency, unused. The shape of the next requirement is visible from here.

Decision

Introduce a LibraryRepository interface with save(Library) and load(), implemented by FileLibraryRepository. Library keeps only the collection behaviour and exposes getBooks() as an unmodifiable view for a repository to read.

load() returns a new Library rather than filling in an existing one.

Consequences


7. The console takes its streams as constructor parameters

Date: 2026-09-02

Status

Accepted

Context

The supplied main was a ninety-line static method that read System.in and wrote System.out directly, with the menu loop, the prompts, the state file and the wiring all in one scope.

Nothing in it could be tested below the level of the whole application. The golden master reaches it by swapping System.in and System.out globally, which works but is process-wide: two such tests cannot run in parallel, and a failure points at the transcript rather than at a method.

Decision

LibraryConsole takes its Library, its Scanner and its PrintStream as constructor parameters and touches no global. Main becomes wiring: load, run, save.

Consequences


8. The menu is an enum, rendered from its own values

Date: 2026-09-02

Status

Accepted

Context

The supplied code held the menu in two places that had to agree by hand: a MENU string listing six numbered lines, and six public static final int constants used by the switch. Nothing tied them together. A seventh option meant editing the string, adding a constant and adding a case, and forgetting any one of the three produced an option that was advertised but dead, or implemented but invisible.

switch on an int also has no exhaustiveness: every value outside 1–6 is a default, so the compiler cannot say whether every option is handled.

Decision

One MenuOption enum carrying each option's code and label. The menu is rendered from values(), and the console switches on the enum.

Consequences