Skills for creating, testing, reviewing, migrating and guarding Spring Boot 4 projects with Java 25: project scaffolding with architecture patterns, Boot 4 test templates, code review, Boot 3 to 4 migration, and a team harness of build gates and agent hooks.
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Read the Java version from the build file first. Flag a feature only when the project's version supports it. For Java 26+ targets and preview-API changes, read ../../creating-springboot-projects/references/java-version-policy.md.
Status: Final since Java 21 (JEP 444).
Recommend virtual threads only when ALL of these are true:
Evidence:
Do NOT recommend them for CPU-bound work, low concurrency, low-traffic services, or pools that are sized on purpose (rate limiting, backpressure).
JEP 491, delivered in JDK 24:
synchronized does NOT pin virtual threads. Do not flag synchronized as a pinning risk. Do not ask for a rewrite to ReentrantLock. Code that already uses ReentrantLock can stay as it is.synchronized around blocking I/O pins the carrier thread. Flag it, and suggest ReentrantLock.// Java 21-23 only: pins the carrier
synchronized (lock) {
String response = httpClient.send(request);
}
// Fix on Java 21-23
lock.lock(); // private final ReentrantLock lock = new ReentrantLock();
try {
String response = httpClient.send(request);
} finally {
lock.unlock();
}On all versions, keep lock scopes narrow and avoid blocking I/O while holding a lock. That is a contention issue, not a pinning issue.
Diagnostics: -Djdk.tracePinnedThreads has no effect on JDK 24+. Use the JFR event jdk.VirtualThreadPinned.
spring.threads.virtual.enabled=true.@Async methods run on them.All of these are final. Flag the old form only when the project's Java version supports the new one.
| Feature (min Java) | Flag this | Suggest this | Notes |
|---|---|---|---|
| Switch expressions (14) | switch statements that assign a variable with break | switch expression with -> and yield | |
| Text blocks (15) | Multi-line SQL/JSON built with + and \n | """ text block, also in @Query | A closing """ on its own line keeps a trailing newline |
instanceof patterns (16) | instanceof followed by a cast | if (obj instanceof String s) | |
| Records (16) | Immutable DTOs with hand-written getters, equals, hashCode | record, with validation in the compact constructor | Use records for DTOs, projections, value objects. Never as JPA entities |
| Sealed types (17) | Fixed hierarchies (events, states) left open | sealed interface ... permits ... | Enables exhaustive switch without default |
| Switch patterns (21) | if/else instanceof chains | switch (obj) { case Integer i -> ... } | case null is allowed |
| Record patterns (21) | Manual accessor calls after a type check | if (obj instanceof Point(int x, int y)) | Also works in switch |
| Sequenced collections (21) | list.get(0), list.get(list.size() - 1), Collections.reverse | getFirst(), getLast(), reversed() | reversed() is a view; on an unmodifiable list it is read-only |
String Templates do not exist. They previewed in JDK 21-22 (JEP 430, 459) and were withdrawn (JEP 465). Flag any STR."..." code. Use formatted() or String.format.
| Feature | Status | Recommend when | Do not recommend when |
|---|---|---|---|
| Foreign Function & Memory API | Final in 22 (JEP 454) | The code calls native C/C++ libraries | A pure-Java solution exists |
Module import declarations (import module java.sql;) | Final in 25 (JEP 511) | Many imports from one module | Most Spring code; normal imports are clearer |
| Flexible constructor bodies | Final in 25 (JEP 513) | Validation or argument prep must run before super(...)/this(...) | A helper method is already clear |
| Scoped values | Final in 25 (JEP 506) | Request context passed down a call tree, mainly with virtual threads; replaces ThreadLocal | The value must change inside the scope |
Key Derivation Function API (javax.crypto.KDF) | Final in 25 (JEP 510) | HKDF key derivation (session keys, sub-keys from a master secret) | Password hashing |
Facts to check in review:
super(...) can validate arguments and assign fields, but cannot read this.ScopedValue.where(KEY, value).run(task), then KEY.get() inside the scope. In JDK 25, ScopedValue.orElse does not accept null.KDF.getInstance("HKDF-SHA256"), HKDFParameterSpec.ofExtract().addIKM(ikm).addSalt(salt).thenExpand(info, 32)). PBKDF2 stays on SecretKeyFactory. bcrypt, scrypt, and Argon2 are not included. For passwords, use Spring Security's PasswordEncoder.Preview features need --enable-preview at compile time and at run time, and the API can change in each release. Suggest them only when the project already uses --enable-preview. Match samples to the project's exact JDK.
JDK 25: StableValue (JEP 502). JDK 26: LazyConstant (JEP 526). JDK 27: third preview (JEP 531).
// JDK 25 only (JEP 502), removed in JDK 26
private final StableValue<DatabaseConfig> config = StableValue.of();
DatabaseConfig getConfig() { return config.orElseSet(this::loadDatabaseConfig); }
// JDK 26 and 27 (JEP 526 / JEP 531)
private final LazyConstant<DatabaseConfig> config = LazyConstant.of(this::loadDatabaseConfig);
DatabaseConfig getConfig() { return config.get(); }Since JDK 26, null results are not allowed, and lazy collections use List.ofLazy and Map.ofLazy. JDK 27 removes isInitialized() and orElse() and adds Set.ofLazy.
Without --enable-preview, a static final holder class or a Spring singleton bean covers most cases.
Preview: JDK 25 (JEP 507), JDK 26 (JEP 530), JDK 27 (JEP 532, unchanged from 26). Allows obj instanceof int i and case int i ->. A pattern does not match when the conversion would lose data: with long v = 1000L, v instanceof byte b is false; with v = 100L it is true.
See performance-patterns.md#structured-concurrency-preview-only.
switch, instanceof + cast, string concatenation for multi-line text, boilerplate DTOs: use the Java 14-21 forms abovelist.get(0) / list.get(list.size() - 1): use getFirst() / getLast()synchronized around blocking I/O. Not a finding on Java 24+ThreadLocal for request context: consider scoped values (Java 25)javax.crypto.KDF (Java 25). Never for passwords--enable-preview