Stage 2 console classes and the shared LoanPolicy, then a Spring Boot 4.1.2 + JPA API (H2, @Version optimistic locking, problem+json errors). Tests write output/ transcripts while asserting the same numbers; scripts regenerate everything. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b4DPL6dtpFg4U4LHc2
4.1 KiB
library-management
Companion code for the ankurm.com article Library Management System in Java: Console Version to Spring Boot REST API. One module, three stages: the same lending rules, first in plain Java, then split into classes, then served over HTTP with Spring Boot 4 and JPA.
Every number, status code and SQL line quoted in the article comes from a file in
output/. The tests write those files while asserting the same numbers, so the
transcripts cannot drift from the code.
Versions
| Component | Version | Verified against |
|---|---|---|
| JDK | 25.0.5 (Temurin, LTS) | java -version |
| Spring Boot | 4.1.2 (4.1.0 went GA on 2026-06-10) | maven-metadata.xml on Maven Central |
| Spring Framework | 7.0.10 | mvn dependency:list |
| Hibernate ORM | 7.4.13.Final | mvn dependency:list |
| Spring Data JPA | 4.1.2 | mvn dependency:list |
| Tomcat (embedded) | 11.0.27 | mvn dependency:list |
| H2 | 2.4.240 | mvn dependency:list |
| Maven | 3.9.11 | mvn -v |
The three stages
| Stage | Where | Run it |
|---|---|---|
| 1. One file, no build tool | LibraryConsole.java (attached to the article, not stored here) |
java LibraryConsole.java < scripts/demo-input.txt |
| 2. Split into classes, rules extracted | src/main/java/com/ankurm/library/console/ + core/LoanPolicy.java |
mvn -q compile && java -cp target/classes com.ankurm.library.console.ConsoleApp < scripts/demo-input.txt |
| 3. Spring Boot REST API with JPA | src/main/java/com/ankurm/library/api/ + core/LoanPolicy.java |
scripts/run.sh |
core/LoanPolicy.java (14-day loans, 3 active loans, Rs 2 a day fine) is shared by stages 2 and 3
and did not change when the app moved to a database.
Quickstart
export JAVA_HOME=/path/to/jdk-25 && export PATH="$JAVA_HOME/bin:$PATH"
mvn test # 6 tests; also regenerates output/02 to output/06
scripts/run.sh # API on http://localhost:8080 (in-memory H2, clean library per start)
scripts/api-demo.sh # in another terminal: curl walk-through
The first Maven run must be online so the plugins are cached; -o works afterwards.
scripts/run-all.sh regenerates every file in output/.
Endpoints
| Method and path | Purpose | Refusals |
|---|---|---|
POST /api/books |
add a book with N copies | 400 bad fields, 409 duplicate ISBN |
GET /api/books |
list books with copies available | |
POST /api/members |
register a member | 400 bad fields, 409 duplicate email |
POST /api/loans |
borrow: {"memberId":1,"bookId":2} |
404 unknown id, 409 no copies, 422 limit or duplicate loan |
POST /api/loans/{id}/return |
return, optional {"returnedOn":"2026-11-20"} |
409 already returned, 422 before borrow date |
GET /api/loans |
all loans, one SQL statement |
Errors are RFC 9457 application/problem+json.
Captured output
| File | What it shows | Produced by |
|---|---|---|
01-console-structured.txt |
stage 2 run of scripts/demo-input.txt (identical to stage 1) |
run-all.sh |
02-loan-policy.txt |
the fine table | LoanPolicyTest |
03-rest-api.txt |
every success and refusal over MockMvc | LibraryApiTest |
04-query-count.txt |
11 SQL statements versus 1 for the same list | QueryCountTest |
05-race-with-version.txt |
8 threads, last copy, @Version: one winner |
BorrowRaceTest |
06-race-without-version.txt |
same race without @Version: 8 loans for 1 copy |
BorrowRaceTest |
07-curl-demo.txt |
the same API on a real Tomcat, driven with curl (dates are the day it ran) | scripts/api-demo.sh |
08-open-in-view-warning.txt |
the Boot default this module overrides | scripts/open-in-view-warning.sh |
09-borrow-sql.txt |
the exact SQL one borrow sends, including where id=? and version=? |
scripts/sql-trace.sh |
Timing-dependent detail: in 05 the 7 refused borrows split between "optimistic-lock failure"
(a thread read the book before the winner committed) and "any other reason" (a thread started late
and found no copy left). Over repeated runs I saw splits ranging from 7/0 to 4/3, which is why the
transcript counts them separately. The invariants (1 winner, 1 loan, 0 copies left) are asserted.