Java Guild course · Lesson 0001
Lesson 0001 · reading the brief
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.
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.
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
~/.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.
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.
JAVA_HOME explicitlyEvery 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.
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
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.
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
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.
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)
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
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
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.
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"
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.
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)
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.
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.
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.
src/test/. It does not exist in the repository you were sent.
Whatever else you do, that directory must be full and meaningful.
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.
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:
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.|| 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.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.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.
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 declared | What is uses it today | What it is telling you |
|---|---|---|
| junit-jupiter 5.10.0 | Nothing | Tests are expected, in JUnit 5, not 4. |
| mockito-core 5.15.2 | Nothing | They expect classes with collaborators — i.e. that you will not leave everything static in Main. |
| sqlite-jdbc 3.47.2.0 | Nothing | The Advanced tier's “portable file-based database”. Note it is compile scope, not test. |
| maven-checkstyle-plugin | Reporting only | Style is graded. The existing code is written to pass sun_checks — final parameters, javadoc on every member. |
| source/target 11 | The compiler | No records, no sealed types, no switch expressions. Work inside it deliberately. |
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.
Why does the presence of an unused exceptions package matter more than any other single detail?
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?
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?
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.
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.”
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.
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
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.
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.