Java Guild course · Lesson 0001

Lesson 0001 · reading the brief

What This Test Is Actually Testing

The README describes a blank page. The repository contains a finished program. Only one of those is the exercise.

Open the README and it reads like a first-year assignment: “Create a simplified Library Management System…”, followed by a list of classes and method signatures. A candidate who reads only that will start typing class Book and never notice that Book already exists, fully written, in the repository they were sent.

This lesson gets you to the point where you can describe, out loud, what Solirius is really asking for — before you write a line of code.

The reframe

This is not a greenfield exercise. It is a legacy-code exercise: here is a working program with defects in it, extend it and improve it, and show us your judgement. The README's method list is a description of what you were given, not a specification of what to build.

Step 0 · Get your toolchain right

Two minutes now saves an hour of confusing output later. Run these and compare against what they should say.

java --version
mvn --version
openjdk 25.0.3 2026-04-21 LTS
Apache Maven 3.9.16
Java version: 25.0.3, vendor: Eclipse Adoptium
Shorthand

~/.local/maven/bin is already on your PATH, so plain mvn works everywhere this course writes ~/.local/maven/bin/mvn. The long form is used only so the commands stay unambiguous about which Maven is running.

The line that actually matters

It is the Java version: line in mvn --version, not the output of java --version. Maven compiles with the JDK it resolves, which is not necessarily the java on your PATH. If those two ever disagree, believe Maven.

Why the course sets JAVA_HOME explicitly

Every command in this course is prefixed with:

export JAVA_HOME=$(/usr/libexec/java_home -v 25)

That is not superstition. jenv is installed here and wired into ~/.zshrc, but no jenv plugins are enabled, which means jenv does not set JAVA_HOME:

$ echo $JAVA_HOME
(empty)
$ which java
/Users/tomspencer/.jenv/shims/java

Maven falls back to the java on PATH, so it resolves correctly anyway — but relying on a fallback is how you end up compiling with a different JDK than you think. Setting it explicitly removes the question.

Using jenv to switch Java version

Three scopes, narrowest wins:

jenv versions                 # list what is registered; * marks the active one

jenv global  21.0.5           # default everywhere
jenv local   21.0.5           # this directory and below (writes .java-version)
jenv shell   21.0.5           # this terminal session only

To undo each, in the same order:

jenv global  25.0.3           # put the default back
jenv local --unset            # delete the .java-version file here
jenv shell --unset            # drop the session override
Right now you have nothing to switch to

jenv versions lists system, 25, 25.0, 25.0.3 and temurin64-25.0.3. That looks like four JDKs. It is one — all four are symlinks to:

/Library/Java/JavaVirtualMachines/temurin-25.jdk/Contents/Home

jenv registers each JDK under several aliases so you can ask for 25 or 25.0.3 interchangeably. There is no older JDK here to fall back to.

If you want a second JDK to switch between

brew install --cask temurin@21

jenv add /Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home
jenv versions                 # 21.x should now appear

cd ~/Desktop/java-guild-candidate-test-intermediate-main
jenv local 21.0.5             # just this project
jenv local --unset            # and back to 25
But you do not need to

The project compiles with maven.compiler.release 11, and JDK 25’s javac accepts --release 8, 11, 17, 21 and 25 — verified. Building on 25 while targeting 11 is exactly right, and it is the stricter test: if it compiles cleanly here it compiles on any assessor’s JDK from 11 upwards.

If you do add one, make it a jenv local, never a jenv global — a project-scoped choice is easy to undo and cannot surprise you in another repository six weeks from now.

Switching Maven version — jenv cannot do this

jenv manages Java only. It ships a maven plugin, but that only makes Maven respect jenv’s Java choice; it does not install or switch Maven versions. There is no mvn anywhere under ~/.jenv/versions/.

You want 3.9.16 rather than 3.9.9, because 3.9.9 on JDK 25 prints warnings from its own bundled libraries before every build:

WARNING: java.lang.System::load has been called by org.fusesource.jansi.internal.JansiLoader
         (file:/Users/tomspencer/.local/maven/lib/jansi-2.4.1.jar)
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by
         com.google.common.util.concurrent.AbstractFuture$UnsafeAtomicHelper
         (file:/Users/tomspencer/.local/maven/lib/guava-33.2.1-jre.jar)

None of that is your project. Those jars live in ~/.local/maven/lib/. Two commands prove it:

mvn --version                          # no project involved at all
... still prints the warnings

mvn dependency:tree | grep -iE 'jansi|guava'
(nothing - the project depends on neither)

Option A — upgrade Maven in place (do this)

cd /tmp
curl -LO https://dlcdn.apache.org/maven/maven-3/3.9.16/binaries/apache-maven-3.9.16-bin.tar.gz
tar xzf apache-maven-3.9.16-bin.tar.gz

mv ~/.local/maven ~/.local/maven-3.9.9-backup   # keep the old one
mv /tmp/apache-maven-3.9.16 ~/.local/maven

mvn --version

Expect Apache Maven 3.9.16 and no WARNING lines at all — verified. Every command in this course keeps working, because the path is the same. To roll back:

rm -rf ~/.local/maven
mv ~/.local/maven-3.9.9-backup ~/.local/maven

Option B — add a Maven Wrapper to the project

Pins the Maven version inside the repository, so the assessor builds with the version you tested with and needs no Maven installed at all:

cd ~/Desktop/java-guild-candidate-test-intermediate-main
mvn wrapper:wrapper -Dmaven=3.9.16

./mvnw --version        # downloads 3.9.16 on first use
./mvnw clean test

Verified: Apache Maven 3.9.16, zero warnings, build green. It adds mvnw, mvnw.cmd and .mvn/wrapper/maven-wrapper.properties:

git add mvnw mvnw.cmd .mvn/
git commit -F - <<'EOF'
build: add a Maven wrapper pinned to 3.9.16

Lets the project be built with a known Maven version without requiring
one to be installed, so "./mvnw clean test" behaves identically for any
reviewer. 3.9.16 also avoids the native-access and sun.misc.Unsafe
warnings that older Maven releases emit on recent JDKs.
EOF
Should you ship the wrapper?

Standard practice on most modern Java projects, and generally read as a courtesy. The argument against is that it adds three files to a small submission and the brief did not ask for it. Either is defensible — but if you include it, say why in the README, and use ./mvnw consistently in your build instructions rather than mixing the two.

If you would rather change nothing

This silences the four jansi warnings but not the four guava ones, which need the newer Maven. Undo with unset MAVEN_OPTS.

export MAVEN_OPTS="--enable-native-access=ALL-UNNAMED"
None of this is a defect in the submission

These warnings come from the build tool, not the code, and will not cost you a mark. Fix them because a clean build output makes real problems visible — not because the assessor will see them.

Do this now, before reading on

Task · 10 minutes

Build it, then break it with your own hands. Three commands:

cd ~/Desktop/java-guild-candidate-test-intermediate-main
export JAVA_HOME=$(/usr/libexec/java_home -v 25)
~/.local/maven/bin/mvn clean test

Now compile once and run the program twice, each time feeding it input a real user could plausibly type:

mvn -q clean compile

# A. borrow a book the library does not have
printf '4\nGone With The Wind\n6\n' | java -cp target/classes com.solirius.intermediate.library.Main

# B. type something that is not a number at the menu
printf 'banana\n6\n' | java -cp target/classes com.solirius.intermediate.library.Main

Come back when you have seen both stack traces with your own eyes.

Both crash, which is the point. From A:

Enter the title of the book to borrow: Exception in thread "main"
  java.lang.RuntimeException: java.io.IOException: Book not found: Gone With The Wind
	at com.solirius.intermediate.library.Main.main(Main.java:107)
Caused by: java.io.IOException: Book not found: Gone With The Wind
	at com.solirius.intermediate.library.Library.borrowBook(Library.java:75)

From B:

Enter your choice: Exception in thread "main" java.util.InputMismatchException
	at java.base/java.util.Scanner.nextInt(Scanner.java:2251)
	at com.solirius.intermediate.library.Main.main(Main.java:72)
Notice what is missing from both

No Saving library state... and no Thank you for using the Library Management System!. The application died before reaching either. A single typo throws away everything the user did in that session, because state is written only on menu option 6.

Why not mvn exec:java?

Because it cannot work on this project. exec-maven-plugin is not configured in the pom.xml, so it has no mainClass to run and fails with:

[ERROR] The parameters 'mainClass' for goal
        org.codehaus.mojo:exec-maven-plugin:3.6.3:java are missing or invalid

Running java -cp target/classes directly is simpler, faster, and is what your README should tell the assessor to do anyway.

What the build tells you

Here is the interesting line from mvn test on the code as supplied:

[INFO] --- surefire:3.2.5:test (default-test) @ java-guild-candidate-test ---
[INFO] No tests to run.
[INFO] BUILD SUCCESS

The brief's Graduate tier — the tier every candidate must clear before their own tier counts — asks for “Unit Tests: Write test cases for Book and Library classes”. There are none. Zero. The build is green because there is nothing to be red about.

The first thing an assessor greps for

src/test/. It does not exist in the repository you were sent. Whatever else you do, that directory must be full and meaningful.

The loudest signal in the repository

Look at what is in src/main/java/…/library/exceptions/:

exceptions/
├── AlreadyBorrowedException.java
├── BookNotFoundException.java
├── NotBorrowedException.java
└── package-info.java

Three exception classes, each fully written with javadoc. Now grep the rest of the source for any of those names:

$ grep -rn "AlreadyBorrowedException\|BookNotFoundException\|NotBorrowedException" \
    src/main/java --include=*.java | grep -v "^src/main/java/.*exceptions/"
(no output)

Not one of them is referenced anywhere. They are imported by nothing, thrown by nothing, caught by nothing. They are dead code.

Now read the Intermediate requirement again, slowly:

Exception Handling: Handle scenarios where a book is already borrowed, does not exist, or cannot be returned.

Three scenarios. Three exception classes, named after exactly those three scenarios, sitting unused in the repository. This is not an accident and it is not a coincidence. It is the exercise, left on the table with a note on it.

What the code does instead

Here is Library.borrowBook as supplied:

public boolean borrowBook(final String title) throws IOException {
    Book book = searchBook(title);
    if (book == null || !book.borrowBook()) {
        throw new IOException("Book not found: " + title);
    }
    return true;
}

Three separate things are wrong with those five lines, and you should be able to name all three in one breath:

  1. The exception type is a lie about the category of failure. IOException means a stream, a file, a socket failed. Nothing here performs I/O. A caller who sees IOException in the signature will reasonably conclude this method touches a disk.
  2. The message is a lie about the facts. The || collapses two entirely different situations — no such book and the book is here but already on loan — into one branch with one message. Borrow the same book twice and the library tells you it has never heard of it.
  3. The return value is dead. The method returns boolean, but the only path that reaches return returns true. It can never return false. Every caller writing if (library.borrowBook(t)) is writing a branch that cannot be taken.
Verified, not guessed

Every claim in this course was reproduced against the real starter code on 2026-08-30. Borrowing an already-borrowed Dune really does produce:

java.io.IOException: Book not found: Dune

The book is right there. The library is lying to you. See the defect inventory for the full list with reproduction steps.

Read the pom as a hint sheet

Most candidates skim pom.xml for the Java version and move on. Read it as a statement of what a good answer contains:

What is declaredWhat is uses it todayWhat it is telling you
junit-jupiter 5.10.0NothingTests are expected, in JUnit 5, not 4.
mockito-core 5.15.2NothingThey expect classes with collaborators — i.e. that you will not leave everything static in Main.
sqlite-jdbc 3.47.2.0NothingThe Advanced tier's “portable file-based database”. Note it is compile scope, not test.
maven-checkstyle-pluginReporting onlyStyle is graded. The existing code is written to pass sun_checks — final parameters, javadoc on every member.
source/target 11The compilerNo records, no sealed types, no switch expressions. Work inside it deliberately.
The Java 11 constraint is real

Verified on this machine: javac --release 11 rejects records outright —

error: records are not supported in -source 11
  (use -source 16 or higher to enable records)

JDK 25's compiler still accepts --release 8, 11, 17, 21 and 25, so staying on 11 is safe on any assessor's machine. Knowing precisely what you gave up — and saying so — scores better than quietly bumping the version.

Drill · what is this repository asking for?

Why does the presence of an unused exceptions package matter more than any other single detail?

  1. Unused classes always fail a Checkstyle report and must be deleted first
  2. Dead code in a repository is the defect the assessors want removed here
  3. It maps one-to-one onto the three scenarios your tier must handle
  4. Custom exceptions are required whenever a method can fail in Java code

Checkstyle's Sun rules do not flag unused classes, and deleting them would be the exact opposite of what is wanted.

Deleting them would throw away the strongest hint in the repository. They are not clutter — they are an unopened gift.

Right. Already borrowed, does not exist, cannot be returned — the Intermediate requirement lists three scenarios and the repository ships three exception classes with matching names. Wiring them up is the single highest-value change you can make.

Plenty of failures are properly signalled by a return value or an Optional. The argument for exceptions here is specific to this brief, not general.

What is the strongest argument against throws IOException on borrowBook?

  1. Checked exceptions are widely considered a mistake in modern Java design
  2. It describes the wrong abstraction, so callers must know the implementation
  3. It forces every caller to wrap the call in a try-catch block to compile it
  4. IOException is too general a type to carry a meaningful diagnostic message

A real debate, but not this one — you can hold either view and still see that this particular type is wrong. Argue the type, not the category.

This is Effective Java Item 73: throw exceptions appropriate to the abstraction. IOException leaks a claim about file handling into a method that only inspects a list in memory. It makes the caller reason about the implementation to understand the signature.

A domain exception would too, if you keep it checked. The try-catch is not the objection; what the type means is.

It carries the message fine. The problem is that the message is attached to a type that misdescribes what went wrong.

The brief says “visibility at candidate discretion” beside every class and method. Why?

  1. Design decisions are being marked, so they refuse to make them for you
  2. The assessors have no strong preference between public and private members
  3. Different Java versions enforce different default visibility rules for fields
  4. It allows candidates to submit the work as a module with exported packages

Read it alongside the evaluation criterion “proper use of object-oriented programming principles”. They are declining to specify encapsulation because encapsulation is the thing being assessed. A brief that told you to make books private would be marking nothing.

They have a very strong preference — the criteria say so. What they lack is a wish to hand you the answer.

Default visibility rules have not changed. Nothing about Java 11 versus 21 bears on this.

Nothing in the project uses the module system; there is no module-info.java and no requirement for one.

Say it out loud

If an interviewer opens with “so, tell me about the exercise”, the first thirty seconds decide how the rest goes. Rehearse this shape:

“The first thing I noticed was that the README describes building it from scratch, but the repo already has a working implementation — so I read it as a legacy-code exercise instead. There were three exception classes in the repo that nothing referenced, and the Intermediate requirement names exactly those three scenarios, so that told me where to start. And mvn test said no tests to run, which is the Graduate requirement unmet.”

Before you change anything · put it under git

Step 1 of the whole exercise

Your commit history is part of what gets marked, and the single most valuable commit is the one you make before you touch anything — the unmodified starter. Everything you do afterwards is then legible as a diff against what Solirius handed over.

If you have already started editing, copy your changed files somewhere safe, restore the originals, do this, then put your changes back. It is worth the faff.

Copy this whole block:

cd ~/Desktop/java-guild-candidate-test-intermediate-main

git init -b main
git config user.name  "Tom Spencer"
git config user.email "tomspencerlondon@gmail.com"

git add -A
git commit -F - <<'EOF'
chore: add starter project as supplied

Baseline commit of the unmodified Solirius starter, so that every
subsequent change is visible as a diff against what was handed over.
EOF

git log --oneline

That last line should print one commit. From here on, every lesson ends with its own commit block.

Fix the build warning while you are here

The pom uses maven.compiler.source/target, which makes modern JDKs complain. Replace these two lines in pom.xml:

<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>

with this single line:

<maven.compiler.release>11</maven.compiler.release>

and change the placeholder <groupId>groupId</groupId> to <groupId>com.solirius.intermediate</groupId>. Then:

~/.local/maven/bin/mvn clean test

The -source 11 warning is gone, and the project still targets Java 11. Commit it:

git add pom.xml
git commit -F - <<'EOF'
build: compile with release 11 and set a real groupId

maven.compiler.source/target produce a warning on modern JDKs:

  location of system modules is not set in conjunction with -source 11
  --release 11 is recommended instead

release 11 also guarantees the code is compiled against the Java 11 API,
not merely to its bytecode level. The Java version itself is unchanged:
the project still targets 11, so no records, sealed types or switch
expressions are used anywhere in the submission.

groupId was left as the literal placeholder "groupId".
EOF

Read this

Primary source

The brief itself — README.md in the starter repo. Read it three times. Once for the requirements, once for the Evaluation Criteria at the bottom, and once asking “which sentence in here does the code I was given already violate?” The five evaluation criteria are the marking scheme; everything this course does is aimed at them.

Carry on

Disagree with any of this? Bring it to me. If you think the IOException is defensible, argue it — being able to defend the code you were given is as much a skill as criticising it.