Java
Write to the language level you compile with — records, sealed types and pattern matching by default, and nullness as a build failure.
Verified 2026-07-31 · Against Java 25 (LTS), JSpecify, NullAway / Error Prone, Lombok.
Write to the language level you compile with. Most Java code is written as if it were Java 8 with newer syntax available but unused. Records, sealed types and pattern matching remove entire categories of boilerplate and whole classes of bug — use them by default, and reach for a library only when the language genuinely cannot express it.
Defaults
- Records for every immutable carrier of data — DTOs, domain models, events,
value objects — with validation annotations directly on components
(
@NotBlank String code). Jackson and Bean Validation both handle record components. A DTO is never a@Getter/@Setterbean. - Compact constructors for defaulting and invariants — not an init method, not a factory.
- Exhaustive
switchexpressions with pattern matching, includingcase null. Never adefaultbranch that silently swallows a newly added enum constant — that branch is how a new state ships to production doing nothing. - Sealed interfaces to model a closed set of alternatives, so
switchproves exhaustiveness at compile time. - Text blocks for SQL and JSON.
varwhere the initialiser already states the type. Optionalas a return type at port boundaries only — never a field, never a parameter.- Lombok only for what a record cannot remove:
@RequiredArgsConstructoron Spring beans,@Builderon wide records. It is not a substitute for records. - Mutable classes only where a framework demands one. JPA
@Entityis the standing exception.
Exhaustive switch over a sealed hierarchy is the
developer lens question made mechanical: does
the type system still catch additions? Once the values become runtime data
instead, a publication check has to fail in its place — see
the configurability boundary.
Domain modelling
- A lifecycle transition is a method on the model —
order.confirm()— returning a new instance and throwing when the rule is broken. Never encode a transition rule in SQL, a service or a controller: a rule you cannot exercise without a database is a rule nobody tests. - Domain errors as an enum beside the model, implementing a shared error interface.
- An operation implements a contract; it does not carry a marker. An
annotation is metadata
javacignores, so the shape it claims is unenforced. An interface makes it a compile error — see the operation. - Register the bean with
@Named(JSR-330), not@Service. Spring scans it natively, and it keepsorg.springframeworkout of the core: the transaction annotation is the only framework type an operation should name. - The outer type owns the conversion. An entity carries
from()/toDomain()/toDto(); a request record carriestoModel(). No mapper class, no mapper package, no MapStruct. - A use case always answers with an object, never a bare
UUID,Stringor collection, and nevervoid. The response is JSON, so it is an object — and an object gains a field without breaking any client. A transition answers with what changed and when;204stays the adapter's choice. - The actor is a field on the command, never on the request body. The command
is not deserialized, so it can carry the caller safely; the request record stays
a pure payload with no write-only
@JsonIgnorefields. See the operation for why the olderexecute(input, actor)rule existed and what replaces it.
Nullness — make it a build failure
JSpecify + NullAway as an Error Prone plugin: -Xep:NullAway:ERROR, JSpecify
mode, @NullMarked per package via package-info.java. A nullness mistake then
fails the build, not just the IDE — which is the only version of this that
actually holds.
- Disable every other Error Prone check (
-XepDisableAllChecks) so the signal stays on nullness. Turn one on deliberately, with the finding it is meant to catch. - Exclude test sources. A test passing
nullon purpose is doing its job.
Trap: Error Prone on JDK 25 needs --add-exports flags in .mvn/jvm.config.
Without them javac fails with the useless message "An unknown compilation problem
occurred", which points nowhere near the cause and burns an afternoon.
Formatting
One formatter, enforced in CI — Spotless over Java, POM, YAML, JSON and Markdown. Do not fight it, apply it. Formatting arguments in review are pure waste.