Architect
Standards
ReferenceNormative

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/@Setter bean.
  • Compact constructors for defaulting and invariants — not an init method, not a factory.
  • Exhaustive switch expressions with pattern matching, including case null. Never a default branch 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 switch proves exhaustiveness at compile time.
  • Text blocks for SQL and JSON. var where the initialiser already states the type.
  • Optional as a return type at port boundaries only — never a field, never a parameter.
  • Lombok only for what a record cannot remove: @RequiredArgsConstructor on Spring beans, @Builder on wide records. It is not a substitute for records.
  • Mutable classes only where a framework demands one. JPA @Entity is 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 modelorder.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 javac ignores, 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 keeps org.springframework out 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 carries toModel(). No mapper class, no mapper package, no MapStruct.
  • A use case always answers with an object, never a bare UUID, String or collection, and never void. 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; 204 stays 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 @JsonIgnore fields. See the operation for why the older execute(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 null on 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.

On this page