# Java Style Guide

> Conventions for writing, structuring, formatting and shipping modern Java 23 that compiles with zero warnings and no surprises.

Revision 2026.10, released 2026-10-19. This pinned URL never changes; the latest text is at https://style.chopper.dev/java/llms.txt. Changelog: https://style.chopper.dev/java/changelog.md.

Rule levels: MUST / MUST NOT (non-negotiable), SHOULD / SHOULD NOT (the default; deviate only with a short comment saying why), MAY (allowed when it helps), AVOID (allowed but a smell). In this document they appear as a bracketed prefix on each rule, after its stable ID, e.g. "`JAVA-02.1` [MUST] ...". Cite a rule by its ID (JAVA-02.1): IDs never change or get reused, and https://style.chopper.dev/java@2026.10#JAVA-02.1 links to the rule. Code samples use `com.example.project` as the base package and module name, and `Project` as the project name. Both are placeholders; substitute your own.

## Starter project

A starter project that follows this guide exists, but templates are not versioned: see the latest guide at https://style.chopper.dev/java/llms.txt for its download URL and parameters.

## Contents

- 01. Purpose and scope
- 02. Language baseline and toolchain
- 03. Repository and project layout
- 04. Source file structure
- 05. Naming
- 06. Formatting
- 07. Modern language features
- 08. Classes and interfaces
- 09. Methods
- 10. Null handling
- 11. Exceptions and errors
- 12. Collections and streams
- 13. Concurrency
- 14. Generics and API design
- 15. Modules and packages
- 16. Logging and diagnostics
- 17. Testing
- 18. Documentation
- 19. Build and static analysis
- 20. Performance
- 21. Git and review
- A. Appendix

## 01. Purpose and scope

Every Java project that adopts this guide targets Java 23, formats with google-java-format, and builds with zero compiler warnings. The goal is that any two projects read as if one person wrote them.

The guide covers language level, layout, naming, formatting, modern language features, null handling, errors, concurrency, testing, documentation, builds and commits. It does not cover per-project architecture; each project documents that in its own `docs/` folder.

**Rule levels.** Each rule carries one of these words:

| Level | Meaning | Breaking it |
| --- | --- | --- |
| [MUST] [MUST NOT] | Non-negotiable. Tooling or review enforces it. | Needs a written reason in the code and the commit. |
| [SHOULD] | The default. Expected in nearly all code. | Fine with a short comment saying why. |
| [MAY] | Allowed when it helps; neither encouraged nor discouraged. | Nothing to break. |
| [AVOID] | Allowed but a smell. | Fine when the alternative is clearly worse. |

**Examples.** Code samples use `com.example.project` as the base package and module name, and `Project` as the project name. Both are placeholders; substitute your own.

**Google Java Style Guide.** Formatting, naming, imports, source file structure and Javadoc rules are adapted from the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html) (CC BY 3.0). Its examples are rewritten here.

**When the guide is silent,** match the surrounding code, then the Google Java Style Guide, then *Effective Java* (3rd edition), in that order.

## 02. Language baseline and toolchain

The language level is Java 23, compiled with `--release 23`, and production code uses only final (non-preview) features. Preview features change between releases, and code that depends on them breaks on the next JDK.

- `JAVA-02.1` [MUST] compile with `--release 23`, not `-source` / `-target`, so the build cannot accidentally use APIs newer than 23.
- `JAVA-02.2` [MUST] build warning-free with `-Xlint:all -Werror` (individual lints may be turned off in the build file with a comment saying why).
- `JAVA-02.3` [MUST NOT] enable `--enable-preview` in production modules. Preview and incubator features are allowed only in throwaway prototypes and benchmark modules.
- `JAVA-02.4` [MUST] pin the JDK vendor and version in `.java-version` / `.sdkmanrc` and in CI. Use a current Java 23 update build of a mainstream distribution (Temurin, Corretto, Zulu).
- `JAVA-02.5` [SHOULD] plan the move to the next LTS (Java 25) as one tracked task rather than drifting. Java 23 is a non-LTS release with a short public support window.

**What Java 23 adds, and the policy for each** ([JDK 23 project page](https://openjdk.org/projects/jdk/23), GA 17 September 2024)

| JEP | Feature | Status in 23 | Policy |
| --- | --- | --- | --- |
| **467** | Markdown documentation comments (`///`) | Final | Use for all new doc comments (section 18) |
| **474** | Generational ZGC by default | Final | Default collector choice for low-latency services |
| **471** | Deprecate `sun.misc.Unsafe` memory access | Deprecation | Do not use; prefer `VarHandle` and the FFM API |
| **455** | Primitive types in patterns | Preview | Not in production |
| **466** | Class-File API | 2nd preview | Not in production |
| **473** | Stream gatherers | 2nd preview | Not in production |
| **476** | Module import declarations | Preview | Not in production |
| **477** | Implicitly declared classes, instance `main` | 3rd preview | Scripts and teaching only |
| **480** | Structured concurrency | 3rd preview | Not in production |
| **481** | Scoped values | 3rd preview | Not in production |
| **482** | Flexible constructor bodies | 2nd preview | Not in production |
| **469** | Vector API | 8th incubator | Not in production |

**Final features from earlier releases that this guide builds on:** records, sealed classes, pattern matching for `instanceof` and `switch`, record patterns, switch expressions, text blocks, `var`, virtual threads, sequenced collections, the Foreign Function & Memory API, and unnamed variables and patterns (`_`).

## 03. Repository and project layout

Every repository is a Maven multi-module build with one parent POM, a `core` module holding the domain, thin modules on top for each way the code is delivered, and packages organized by feature rather than by layer.

```text
project/
├── pom.xml                       # parent: versions, plugins, compiler flags
├── .editorconfig
├── .java-version                 # pinned JDK (e.g. 23)
├── mvnw, mvnw.cmd, .mvn/         # Maven wrapper, committed
├── docs/                         # architecture notes, ADRs
├── project-core/                 # domain logic, no framework imports
│   ├── pom.xml
│   └── src/
│       ├── main/java/
│       │   ├── module-info.java
│       │   └── com/example/project/
│       │       ├── billing/      # one package per feature
│       │       │   ├── Invoice.java
│       │       │   ├── InvoiceService.java
│       │       │   └── InvoiceRepository.java
│       │       └── shipping/
│       ├── main/resources/
│       └── test/java/
│           └── com/example/project/billing/
│               └── InvoiceServiceTest.java
├── project-api/                  # HTTP or RPC adapter
├── project-cli/                  # command-line entry point
└── project-it/                   # integration tests (Testcontainers)
```

- `JAVA-03.1` [MUST] follow the standard Maven directory layout (`src/main/java`, `src/test/java`, `src/main/resources`). Every tool, IDE and plugin assumes it.
- `JAVA-03.2` [MUST] put each test class in the same package as the class it tests, under `src/test/java`, named `<Name>Test`: `InvoiceService` is tested by `InvoiceServiceTest`. Same package gives tests package-private access without widening visibility.
- `JAVA-03.3` [SHOULD] organize packages by feature (`billing`, `shipping`), not by layer (`controllers`, `services`, `models`). A feature package holds its types, its service and its repository together.
- `JAVA-03.4` [SHOULD] keep `project-core` free of framework imports (Spring, Jakarta, Micronaut). Adapters depend on core, never the reverse.
- `JAVA-03.5` [MUST] commit the Maven wrapper so a fresh clone builds with `./mvnw verify` and no global install.
- `JAVA-03.6` [AVOID] `util`, `common` or `helpers` packages. A helper belongs to the feature that needs it; promote it to a shared package only when three features need it.
- `JAVA-03.7` [MUST NOT] check in generated sources, `target/` directories or IDE files (`.idea/`, `*.iml`, `.vscode/`).

## 04. Source file structure

A source file holds exactly one top-level type, is named after it, and is laid out in a fixed order: license header, package, imports, the type. Sections are separated by exactly one blank line.

- `JAVA-04.1` [MUST] name the file after its single top-level type, case-sensitively: `InvoiceService.java`.
- `JAVA-04.2` [MUST] encode source files in UTF-8 and indent only with ASCII spaces, never tabs.
- `JAVA-04.3` [MUST] keep the `package` and `import` lines unwrapped, whatever their length.
- `JAVA-04.4` [MUST NOT] use wildcard imports (`import java.util.*;`), static or otherwise, or module imports.
- `JAVA-04.5` [MUST] put static imports in one group, then a blank line, then all other imports in a second group, each sorted in ASCII order. google-java-format does this.
- `JAVA-04.6` [MUST] import nested classes with a normal import, not a static one.
- `JAVA-04.7` [SHOULD] use static imports only where they read well and are idiomatic: test assertions, `Collectors.*` methods, and well-known constants. Avoid static imports of ordinary methods from your own code.
- `JAVA-04.8` [MUST] use special escape sequences (`\n`, `\t`, `\"`) rather than octal or Unicode escapes for them.
- `JAVA-04.9` [SHOULD] write non-ASCII characters literally when that is clearer, and as a Unicode escape (with a comment) when the character is invisible or confusable.

```java
// Copyright notice, if the project uses one.
package com.example.project.billing;

import static java.util.Objects.requireNonNull;

import com.example.project.money.Money;
import java.time.Clock;
import java.util.List;
import org.jspecify.annotations.NullMarked;

/// Creates invoices for completed orders.
@NullMarked
public final class InvoiceService {
  // ...
}
```

## 05. Naming

Names follow standard Java conventions with no prefixes or suffixes: UpperCamelCase for types, lowerCamelCase for members and locals, and UPPER_SNAKE_CASE only for true constants. A reader should know what kind of thing a name is from its shape alone.

| Kind | Style | Example |
| --- | --- | --- |
| Package, module | all lowercase, words run together, no underscores | `com.example.project.billing` |
| Class, record, enum, interface | UpperCamelCase noun or noun phrase | `Invoice`, `PaymentGateway` |
| Interface describing a capability | UpperCamelCase adjective allowed | `Comparable`, `Retryable` |
| Test class | Name of the class under test + `Test` | `InvoiceServiceTest` |
| Method | lowerCamelCase verb or verb phrase | `issueInvoice()`, `isOverdue()` |
| Test method | lowerCamelCase; `_` may separate logical parts | `issue_rejectsEmptyOrder()` |
| Constant (`static final`, deeply immutable) | UPPER_SNAKE_CASE | `MAX_RETRIES`, `DEFAULT_TIMEOUT` |
| Non-constant field | lowerCamelCase, no `m` / `_` prefix | `retryPolicy` |
| Parameter, local variable | lowerCamelCase | `orderId`, `lineItems` |
| Unused variable or pattern | `_` | `catch (NumberFormatException _)` |
| Type variable | One capital letter (+ optional digit), or a name ending in `T` | `T`, `E2`, `RequestT` |
| Enum constant | UPPER_SNAKE_CASE | `PaymentStatus.PARTIALLY_REFUNDED` |

**Rules**

- `JAVA-05.1` [MUST] treat a field as a constant only if it is `static final` and its contents are deeply immutable. A `static final List<String>` from `ArrayList` is not a constant; a `static final Logger` is not a constant. Neither gets UPPER_SNAKE_CASE.
- `JAVA-05.2` [MUST NOT] style `final` locals or parameters as constants.
- `JAVA-05.3` [MUST] use only ASCII letters and digits in identifiers, with underscores only where the table allows.
- `JAVA-05.4` [SHOULD] name booleans as predicates: `isActive`, `hasLineItems`, `canRetry`.
- `JAVA-05.5` [SHOULD] name methods that return a value without side effects as nouns or `get`/`is` queries, and methods that change state as verbs.
- `JAVA-05.6` [AVOID] one-character parameter names in public methods.

### Choosing names

A name should make its purpose clear to a new reader, even one from outside the project. Do not save horizontal space at the cost of that clarity.

- `JAVA-05.7` [SHOULD] make a name as descriptive as its scope is wide. A public method carries its domain in the name; a counter in a five-line loop can be `i`.
- `JAVA-05.8` [SHOULD NOT] repeat what the context already says. An `Invoice` field is `total`, not `invoiceTotal`.
- `JAVA-05.9` [MUST NOT] abbreviate by deleting letters inside a word: `customerId`, not `cstmrId`.
- `JAVA-05.10` [MUST] write acronyms as one capitalized word: `XmlHttpRequest`, `customerId`, `supportsIpv6`, not `XMLHTTPRequest` or `customerID`.
- `JAVA-05.11` [AVOID] abbreviations a reader outside the project would not know. Universal ones (`id`, `url`, `json`, `i`, `T`) are fine.

## 06. Formatting

google-java-format decides formatting, run through Spotless on every build, and nobody argues with it in review. The rules below are what it enforces, so a reader knows what to expect.

| Topic | Rule |
| --- | --- |
| Indentation | 2 spaces per block level, never tabs. |
| Continuation lines | At least +4 spaces from the line they continue. |
| Column limit | 100 characters. |
| Braces | K&R: `{` at the end of the line, `}` on its own line; `} else {` and `} catch (...) {` together. |
| Control statements | `if`, `else`, `for`, `do`, `while` always braced, even for one statement. |
| Statements | One per line. One variable per declaration. |
| Blank lines | One between members. Multiple blank lines allowed but discouraged. |
| Arrays | Brackets on the type: `String[] args`, never `String args[]`. |
| `long` literals | Uppercase `L`: `3000000000L`. |
| Modifier order | `public protected private abstract default static final sealed non-sealed transient volatile synchronized native strictfp` |
| Alignment | Never align columns by hand. |

### Column limit

Every line is at most 100 characters. A line may run longer only when it is a `package` or `import` line, a line inside a text block, a URL or shell command in a comment that cannot be split, or a very long identifier.

### Line wrapping

- `JAVA-06.1` [SHOULD] break at the highest syntactic level available.
- `JAVA-06.2` [MUST] break before an operator, a `.`, a `::`, the `&` in a type bound, or the `|` in a multi-catch; breaking after `=` is also allowed.
- `JAVA-06.3` [MUST] keep a method or constructor name attached to its `(`, and a comma attached to the token before it.
- `JAVA-06.4` [MUST NOT] break next to a lambda or switch arrow `->`, except right after it when a single unbraced expression follows.

```java
List<Invoice> overdue =
    invoices.stream()
        .filter(invoice -> invoice.dueDate().isBefore(today))
        .sorted(Comparator.comparing(Invoice::dueDate))
        .toList();
```

### Braces and empty blocks

- `JAVA-06.5` [MUST] brace every `if`, `else`, `for`, `do` and `while` body. Lambda bodies may omit braces.
- `JAVA-06.6` [MAY] write an empty block as `{}`, except in a multi-block statement (`if`/`else`, `try`/`catch`/`finally`), where each empty block is written out.

```java
if (invoice.isPaid()) {
  archive(invoice);
} else if (invoice.isOverdue(today)) {
  sendReminder(invoice);
} else {
  scheduleCheck(invoice);
}

try {
  publish(event);
} catch (TransientException e) {
  retryQueue.offer(event);
}
```

### Switch

- `JAVA-06.7` [MUST] use arrow (`->`) switches in new code; they never fall through.
- `JAVA-06.8` [MUST] make every switch exhaustive, adding `default` when the compiler does not require it. Switches over sealed types and enums need no `default` when every case is listed.
- `JAVA-06.9` [MAY] put a short arrow case on one line; a braced case body breaks after `{`.

```java
String label = switch (status) {
  case DRAFT -> "Draft";
  case ISSUED, SENT -> "Awaiting payment";
  case PAID -> "Paid";
  case PARTIALLY_REFUNDED -> {
    log.debug("Partial refund on {}", id);
    yield "Partially refunded";
  }
};
```

### Annotations

- `JAVA-06.10` [MUST] put class, method and constructor annotations on their own lines after the doc comment. A single annotation with no arguments may share the method's line.
- `JAVA-06.11` [MUST] place type-use annotations (such as `@Nullable`) immediately before the type: `@Nullable String note`.
- `JAVA-06.12` [MAY] put several field annotations on one line.

```java
@Override
public String toString() { ... }

@Override public int hashCode() { ... }

@JsonProperty("due_date")
@NotNull
private LocalDate dueDate;
```

### Text blocks

- `JAVA-06.13` [MUST] put the opening `"""` and the closing `"""` on their own lines, with the closing delimiter at the same indentation as the opening one.

```java
String query =
    """
    SELECT id, total
    FROM invoice
    WHERE due_date < ?
    """;
```

## 07. Modern language features

Model data with records, model alternatives with sealed interfaces, and take them apart with pattern-matching `switch`. Together these replace most getter-setter beans, visitor patterns and `instanceof` chains.

### Records

- `JAVA-07.1` [SHOULD] use a `record` for every immutable data carrier: DTOs, value objects, events, map keys, multi-value returns.
- `JAVA-07.2` [MUST] validate and normalize in a compact constructor; a record never holds an invalid state.
- `JAVA-07.3` [MUST] make defensive copies of mutable components (`List.copyOf`, `Map.copyOf`) in the compact constructor.
- `JAVA-07.4` [AVOID] records for entities with identity or mutable state; use a class.

```java
public record Money(BigDecimal amount, Currency currency) {
  public Money {
    requireNonNull(amount, "amount");
    requireNonNull(currency, "currency");
    amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN);
  }

  public Money plus(Money other) {
    checkSameCurrency(other);
    return new Money(amount.add(other.amount), currency);
  }
}
```

### Sealed types and pattern matching

- `JAVA-07.5` [SHOULD] model a closed set of alternatives as a `sealed interface` with `record` implementations.
- `JAVA-07.6` [SHOULD] handle sealed hierarchies with a `switch` over the type, with no `default`, so adding a variant breaks the build everywhere it is not handled.
- `JAVA-07.7` [SHOULD] use record patterns to deconstruct when it makes the case clearer, and `_` for components you do not use.
- `JAVA-07.8` [MUST] use `instanceof` patterns (`if (shape instanceof Circle c)`) instead of casting after a check.

```java
public sealed interface PaymentResult
    permits PaymentResult.Approved, PaymentResult.Declined, PaymentResult.Pending {

  record Approved(String transactionId, Money amount) implements PaymentResult {}

  record Declined(String reason) implements PaymentResult {}

  record Pending(Duration retryAfter) implements PaymentResult {}
}

String describe(PaymentResult result) {
  return switch (result) {
    case Approved(var id, var amount) -> "Approved " + amount + " as " + id;
    case Declined(var reason) -> "Declined: " + reason;
    case Pending(_) -> "Pending";
  };
}
```

### `var`

- `JAVA-07.9` [SHOULD] use `var` when the type is obvious from the right-hand side: a constructor call, a factory method that names its type, or a literal.
- `JAVA-07.10` [SHOULD NOT] use `var` when the type is unclear without an IDE, such as the result of a call to an unfamiliar method.
- `JAVA-07.11` [MUST NOT] use `var` for fields, parameters or return types (the language forbids most of these anyway), or with a diamond: `var list = new ArrayList<>()` infers `ArrayList<Object>`.

```java
var invoices = new ArrayList<Invoice>(); // obvious
var timeout = Duration.ofSeconds(30); // obvious
InvoiceSummary summary = reportBuilder.build(); // not obvious: spell it out
```

### Unnamed variables and patterns

- `JAVA-07.12` [SHOULD] use `_` for any variable or pattern component that is required by the syntax but never read: unused catch parameters, lambda parameters, loop variables, record components.

```java
try {
  return Integer.parseInt(text);
} catch (NumberFormatException _) {
  return defaultValue;
}

map.forEach((_, value) -> process(value));
```

### Enums

- `JAVA-07.13` [SHOULD] use an `enum` for a fixed set of constants with no data per instance beyond a few fields.
- `JAVA-07.14` [MUST NOT] depend on `ordinal()` for persistence or logic; store and compare by name or by an explicit field.
- `JAVA-07.15` [SHOULD] use `EnumSet` and `EnumMap` instead of hash-based collections keyed by an enum.

## 08. Classes and interfaces

Classes are small, `final` unless designed for extension, immutable unless they have a reason to change, and no more visible than they need to be. Most of what a class needs to say is said by its modifiers.

**Visibility and extension**

- `JAVA-08.1` [SHOULD] declare classes `final` unless they are designed and documented for inheritance. Prefer composition over inheritance.
- `JAVA-08.2` [SHOULD] default to package-private for classes and members; make something `public` only when another package needs it.
- `JAVA-08.3` [MUST] keep fields `private` (constants excepted). Expose state through methods.
- `JAVA-08.4` [MUST] annotate every override with `@Override`, unless the overridden method is `@Deprecated`.
- `JAVA-08.5` [MUST NOT] override `Object.finalize()`. Use `Cleaner` or try-with-resources.

**Immutability**

- `JAVA-08.6` [SHOULD] make fields `final` and assign them once in the constructor.
- `JAVA-08.7` [SHOULD] make value types immutable; use a `record` where it fits (section 7).
- `JAVA-08.8` [MUST] copy mutable inputs on the way in and never hand out mutable internals: return `List.copyOf(items)` or an unmodifiable view.

**Construction**

- `JAVA-08.9` [SHOULD] use static factory methods (`Invoice.draft(...)`, `Money.of(...)`) when a name clarifies intent or the method may return a cached or subtype instance.
- `JAVA-08.10` [SHOULD] use a builder when a type has more than about four optional parameters. Do not build one for two or three.
- `JAVA-08.11` [MUST NOT] call overridable methods from a constructor.
- `JAVA-08.12` [MUST] give a utility class of static methods a `private` constructor and mark it `final`. Before writing one, check whether the methods belong on a type.

**Member order**

There is no single required order, but members follow a logical order the author could explain. The default:

1. Static constants
2. Static fields
3. Instance fields
4. Constructors and static factories
5. Public methods, grouped by what they do
6. Private helpers, placed after the first method that uses them
7. Nested types

- `JAVA-08.13` [MUST] keep overloads (methods or constructors with the same name) together, with nothing in between.
- `JAVA-08.14` [MUST NOT] order members by when they were added. New methods go where they belong, not at the end.

**Interfaces**

- `JAVA-08.15` [SHOULD] introduce an interface when there are two or more implementations, or when the boundary must be faked in tests. Do not create `FooImpl` for every `Foo`.
- `JAVA-08.16` [SHOULD] name implementations by what makes them different: `PostgresInvoiceRepository`, `InMemoryInvoiceRepository`.
- `JAVA-08.17` [MAY] use `default` methods for behavior that is genuinely the same for every implementation; avoid them as a way to add state-like behavior.

**equals, hashCode, toString**

- `JAVA-08.18` [MUST] override `equals` and `hashCode` together, or not at all. Records do this for you.
- `JAVA-08.19` [SHOULD] give every class that appears in logs a `toString` that is useful and never prints secrets.
- `JAVA-08.20` [MUST] qualify static member access with the class name, never with an instance: `Duration.ofSeconds(5)`, not `timeout.ofSeconds(5)`.

```java
public final class InvoiceService {
  private static final Duration GRACE_PERIOD = Duration.ofDays(14);

  private final InvoiceRepository repository;
  private final Clock clock;

  public InvoiceService(InvoiceRepository repository, Clock clock) {
    this.repository = requireNonNull(repository);
    this.clock = requireNonNull(clock);
  }

  public Invoice issue(Order order) {
    var invoice = Invoice.draft(order, dueDate());
    repository.save(invoice);
    return invoice;
  }

  private LocalDate dueDate() {
    return LocalDate.now(clock).plus(GRACE_PERIOD);
  }
}
```

## 09. Methods

Methods are short, do one thing, take few parameters, and return results instead of mutating arguments. A method that needs comments to separate its sections should be several methods.

### Write short methods

Prefer small, focused methods. There is no hard limit, but once a method passes about 40 lines, ask whether it can be split without hurting the structure. Short methods are easier to read, change and test.

### Parameters and return values

- `JAVA-09.1` [SHOULD] keep to about 4 parameters. Past that, group related ones into a record: `issue(InvoiceRequest request)`.
- `JAVA-09.2` [AVOID] `boolean` parameters that read as a mystery at the call site (`send(invoice, true)`); use an `enum` or two named methods.
- `JAVA-09.3` [MUST NOT] reassign parameters. Declare a new local instead.
- `JAVA-09.4` [MUST] return an empty collection, never `null`, when there is nothing to return.
- `JAVA-09.5` [SHOULD] return `Optional<T>` from a lookup that may find nothing (section 10). Never return `null` from a method declared to return `Optional`.
- `JAVA-09.6` [SHOULD] prefer guard clauses and early returns over nested `if` blocks.
- `JAVA-09.7` [SHOULD] keep overloads semantically identical; a reader should not need to know which overload ran.
- `JAVA-09.8` [AVOID] varargs on public APIs except where a fixed list of values is the natural call shape (`List.of`, logging).

### Local variables

- `JAVA-09.9` [MUST] declare each local in the narrowest scope possible, as close to its first use as possible, and initialize it where it is declared.
- `JAVA-09.10` [SHOULD] start collections initialized (`List.of(a, b)`) rather than building them with a series of `add` calls when the contents are known.
- `JAVA-09.11` [MAY] mark locals `final`. Neither required nor discouraged; be consistent within a file.

### Lambdas and method references

- `JAVA-09.12` [SHOULD] prefer a method reference when it is clearer than the lambda: `.map(Invoice::total)`.
- `JAVA-09.13` [SHOULD] keep lambdas to a few lines. A longer body becomes a named private method referenced by `this::validateLine`.
- `JAVA-09.14` [MUST NOT] mutate captured state from a lambda passed to a stream or another thread; collect a result instead.

```java
// Guard clauses, a request record, an Optional lookup.
public Invoice issue(InvoiceRequest request) {
  requireNonNull(request);
  if (request.lines().isEmpty()) {
    throw new IllegalArgumentException("An invoice needs at least one line");
  }

  Customer customer =
      customers
          .findById(request.customerId())
          .orElseThrow(() -> new CustomerNotFoundException(request.customerId()));

  return Invoice.draft(customer, request.lines(), dueDate());
}
```

## 10. Null handling

Everything is non-null unless its type says `@Nullable`, and a checker proves it at compile time. Null is allowed only where it is declared, and `Optional` is used only as a return type.

### Annotations and checking

- `JAVA-10.1` [MUST] mark every package `@NullMarked` (JSpecify) in its `package-info.java`, making non-null the default for the whole package.
- `JAVA-10.2` [MUST] mark the exceptions with `@Nullable` from `org.jspecify.annotations`, placed directly before the type: `@Nullable String middleName`.
- `JAVA-10.3` [MUST] run NullAway on Error Prone in every build, with JSpecify mode on, failing the build on any null-safety error.
- `JAVA-10.4` [MUST NOT] mix nullness annotation libraries (`javax.annotation`, JetBrains, Checker Framework) in the same codebase. Use JSpecify only.
- `JAVA-10.5` [MUST] check arguments from outside the null-checked code (public API entry points, deserialized data, framework callbacks) with `Objects.requireNonNull(arg, "arg")` at the boundary.

```java
// src/main/java/com/example/project/billing/package-info.java
@NullMarked
package com.example.project.billing;

import org.jspecify.annotations.NullMarked;
```

### Optional

- `JAVA-10.6` [SHOULD] return `Optional<T>` from a method that can legitimately find nothing, such as a repository lookup.
- `JAVA-10.7` [MUST NOT] use `Optional` for fields, method parameters, collection elements or map values. Use `@Nullable` or an empty collection instead.
- `JAVA-10.8` [MUST NOT] call `Optional.get()`. Use `orElseThrow()`, `orElse`, `map` or `ifPresentOrElse`, so the empty case is handled where it is visible.
- `JAVA-10.9` [SHOULD] use the primitive specializations (`OptionalInt`, `OptionalLong`) for primitive results.

```java
public Optional<Customer> findById(CustomerId id) { ... }

String name = customers.findById(id).map(Customer::displayName).orElse("Unknown customer");
```

### Collections and strings

- `JAVA-10.10` [MUST] use empty collections and empty strings rather than `null` to mean "none".
- `JAVA-10.11` [MUST NOT] put `null` in a collection. `List.of`, `Set.of` and `Map.of` already reject it.

## 11. Exceptions and errors

Unchecked exceptions signal failures the caller cannot reasonably handle; expected, recoverable outcomes are part of the return type. No exception is ever silently swallowed.

| Situation | Tool |
| --- | --- |
| Caller passed a bad argument | `IllegalArgumentException`, `NullPointerException` via `requireNonNull` |
| Object is in the wrong state for the call | `IllegalStateException` |
| A lookup found nothing | `Optional<T>` return |
| Several expected outcomes the caller must handle (approved, declined, pending) | Sealed result type (section 7) |
| Failure the caller cannot fix (I/O down, bug, broken config) | Unchecked domain exception, such as `PaymentGatewayException extends RuntimeException` |
| A condition that cannot happen | `throw new AssertionError("...")` |

**Rules**

- `JAVA-11.1` [SHOULD] throw unchecked exceptions from your own code. Declare a checked exception only when callers are genuinely expected to recover from that specific failure.
- `JAVA-11.2` [MUST NOT] ignore a caught exception. Handle it, log it with context, or rethrow it. If doing nothing is truly correct, name the variable `_` and write a comment saying why.
- `JAVA-11.3` [MUST] keep the cause when translating: `throw new InvoiceStorageException("Saving " + id, e);`.
- `JAVA-11.4` [SHOULD] translate low-level exceptions at the layer boundary into exceptions that make sense to that layer's callers. A billing service does not leak `SQLException`.
- `JAVA-11.5` [MUST] catch the most specific type available. [MUST NOT] catch `Exception`, `Throwable` or `Error` except at a top-level boundary (a request handler, a thread's run loop, `main`) that logs and reports.
- `JAVA-11.6` [MUST] close every `AutoCloseable` with try-with-resources.
- `JAVA-11.7` [MUST NOT] use exceptions for ordinary control flow, or `return` / `throw` from a `finally` block.
- `JAVA-11.8` [MUST] write exception messages that say what failed and with which input, without secrets: `"Invoice INV-1042 has no lines"`, not `"Invalid state"`.
- `JAVA-11.9` [SHOULD] log an exception once, at the place that decides what to do about it, not at every layer it passes through.
- `JAVA-11.10` [SHOULD] use multi-catch for identical handling: `catch (TimeoutException | ConnectException e)`.

```java
public Invoice load(InvoiceId id) {
  try (var connection = dataSource.getConnection();
      var statement = connection.prepareStatement(SELECT_BY_ID)) {
    statement.setString(1, id.value());
    try (var rows = statement.executeQuery()) {
      if (!rows.next()) {
        throw new InvoiceNotFoundException(id);
      }
      return mapper.map(rows);
    }
  } catch (SQLException e) {
    throw new InvoiceStorageException("Loading invoice " + id, e);
  }
}
```

## 12. Collections and streams

Collections are immutable by default, typed by interface, and never null. Streams are for transforming data in a readable pipeline; a plain loop is fine when it reads better.

### Collections

- `JAVA-12.1` [SHOULD] create fixed collections with `List.of`, `Set.of`, `Map.of` and copy incoming ones with `List.copyOf`, which are immutable and reject nulls.
- `JAVA-12.2` [MUST] declare variables, fields, parameters and return types by interface: `List<Invoice>`, not `ArrayList<Invoice>`.
- `JAVA-12.3` [SHOULD] use the sequenced-collection methods (`getFirst()`, `getLast()`, `reversed()`, `addFirst()`) instead of index arithmetic like `list.get(list.size() - 1)`.
- `JAVA-12.4` [SHOULD] pick the implementation deliberately: `ArrayList` by default, `ArrayDeque` for stacks and queues, `LinkedHashMap` when insertion order matters, `EnumMap` / `EnumSet` for enum keys. [AVOID] `LinkedList`, `Vector`, `Hashtable` and `Stack`.
- `JAVA-12.5` [SHOULD] pre-size collections when the final size is known: `new ArrayList<>(orders.size())`.
- `JAVA-12.6` [MUST NOT] modify a collection while iterating over it; use `removeIf`, an iterator's `remove`, or build a new collection.

### Streams

- `JAVA-12.7` [SHOULD] put each stream operation on its own line once the pipeline has more than two steps.
- `JAVA-12.8` [MUST NOT] cause side effects inside `map`, `filter` or `peek`. Side effects go in a terminal `forEach`, or better, in a loop.
- `JAVA-12.9` [SHOULD] use `toList()` (unmodifiable) to finish a stream. Use `Collectors.toCollection(...)` only when the result must be mutable.
- `JAVA-12.10` [SHOULD] use a `for` loop when the logic needs `break`, checked exceptions, several accumulators or index access. Forcing those into a stream makes it harder to read.
- `JAVA-12.11` [AVOID] parallel streams unless a benchmark shows a gain on the real workload; they share the common fork-join pool.
- `JAVA-12.12` [MUST NOT] use Stream Gatherers (`Stream.gather`) in production code; they are a preview API in Java 23.

```java
Map<CustomerId, Money> totalsByCustomer =
    invoices.stream()
        .filter(Invoice::isIssued)
        .collect(
            Collectors.groupingBy(
                Invoice::customerId,
                Collectors.reducing(Money.zero(EUR), Invoice::total, Money::plus)));

Invoice latest = invoices.getLast();
```

## 13. Concurrency

Blocking I/O runs on virtual threads, CPU-bound work runs on a bounded platform-thread pool, and shared mutable state is the exception, guarded explicitly. Prefer immutable data handed between threads over locks.

### Virtual threads

- `JAVA-13.1` [SHOULD] run I/O-bound tasks (HTTP calls, database queries, file reads) on virtual threads: `Executors.newVirtualThreadPerTaskExecutor()`.
- `JAVA-13.2` [MUST NOT] pool virtual threads. Create one per task; they are cheap.
- `JAVA-13.3` [SHOULD] limit concurrency against a scarce resource with a `Semaphore`, not with a small thread pool.
- `JAVA-13.4` [MUST] guard blocking calls in code that runs on virtual threads with `ReentrantLock` rather than `synchronized`. In Java 23 a virtual thread that blocks inside `synchronized` pins its carrier thread and can starve the scheduler.
- `JAVA-13.5` [AVOID] heavy `ThreadLocal` use with virtual threads; millions of threads each holding a cached object add up. Pass context explicitly.
- `JAVA-13.6` [MUST NOT] use structured concurrency or scoped values in production code; both are preview APIs in Java 23.

### Executors and threads

- `JAVA-13.7` [MUST] close every `ExecutorService` with try-with-resources (it is `AutoCloseable` and waits for tasks), or shut it down explicitly.
- `JAVA-13.8` [MUST] name long-lived platform threads with a `Thread.ofPlatform().name("invoice-export-", 0)` factory so they show in thread dumps and JFR.
- `JAVA-13.9` [SHOULD] size CPU-bound pools to the number of cores, and keep CPU-bound work off virtual threads.
- `JAVA-13.10` [MUST NOT] call `Thread.stop`, `suspend` or `resume`, or rely on `Thread.sleep` for coordination. Use latches, futures and queues.
- `JAVA-13.11` [MUST] restore the interrupt flag when catching `InterruptedException` and not rethrowing it: `Thread.currentThread().interrupt();`.

### Shared state

- `JAVA-13.12` [SHOULD] share only immutable objects (records, `List.copyOf`) between threads.
- `JAVA-13.13` [MUST] guard every piece of shared mutable state with one documented mechanism: a lock, an atomic, or a concurrent collection.
- `JAVA-13.14` [SHOULD] use `java.util.concurrent` types (`ConcurrentHashMap`, `AtomicLong`, `LongAdder`, `BlockingQueue`) instead of hand-rolled synchronization.
- `JAVA-13.15` [MUST] document thread safety on every class that may be shared: `/// Thread-safe.` or `/// Not thread-safe; confine to one thread.`
- `JAVA-13.16` [AVOID] `volatile` except for a single flag or reference published by one writer.

```java
List<Receipt> sendAll(List<Invoice> invoices) throws InterruptedException {
  try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<Receipt>> futures =
        invoices.stream().map(invoice -> executor.submit(() -> mailer.send(invoice))).toList();

    var receipts = new ArrayList<Receipt>(futures.size());
    for (Future<Receipt> future : futures) {
      receipts.add(getResult(future));
    }
    return receipts;
  }
}
```

## 14. Generics and API design

Generic code is fully typed with no raw types, and public APIs are designed to be hard to misuse: narrow, immutable, and explicit about ownership and nullness.

### Generics

- `JAVA-14.1` [MUST NOT] use raw types (`List`, `Comparable`) except in `instanceof` checks and class literals.
- `JAVA-14.2` [SHOULD] follow PECS for API parameters: `? extends T` for inputs the method reads from (producers), `? super T` for outputs it writes to (consumers).
- `JAVA-14.3` [MUST NOT] use wildcards in return types; they push complexity onto every caller.
- `JAVA-14.4` [MUST] suppress unchecked warnings with `@SuppressWarnings("unchecked")` on the narrowest declaration possible, usually a single local variable, with a comment explaining why the cast is safe.
- `JAVA-14.5` [SHOULD] prefer `List<T>` over `T[]` in APIs; arrays and generics do not mix well.

```java
static <T extends Comparable<? super T>> T max(Collection<? extends T> items) { ... }

void copyAll(Collection<? extends Invoice> source, Collection<? super Invoice> target) {
  target.addAll(source);
}
```

### Public API rules

- `JAVA-14.6` [SHOULD] expose the smallest surface that does the job. Every public type and method is a promise; keep the rest package-private.
- `JAVA-14.7` [MUST] accept and return interfaces and immutable types (`List`, records), never mutable internals.
- `JAVA-14.8` [SHOULD] prefer domain types to primitives once a value crosses a package boundary: `InvoiceId`, `Money`, `Duration` rather than `String`, `BigDecimal`, `long millis`.
- `JAVA-14.9` [MUST] use `java.time` types for all dates and times (`Instant`, `LocalDate`, `Duration`), never `Date`, `Calendar` or `long` timestamps in APIs.
- `JAVA-14.10` [MUST] use `BigDecimal` (or a `Money` type) for money, never `double`.
- `JAVA-14.11` [MUST] inject time and randomness (`Clock`, `RandomGenerator`) instead of calling `Instant.now()` or `new Random()` inside logic, so tests can control them.
- `JAVA-14.12` [SHOULD] mark an API being phased out with `@Deprecated(since = "1.4", forRemoval = true)` plus a doc comment naming the replacement, and remove it in the next major version.

## 15. Modules and packages

Each Maven module that is a library declares a `module-info.java` that exports only its API packages; everything else stays encapsulated. Inside a module, package-private is the default visibility.

- `JAVA-15.1` [SHOULD] give every library module a `module-info.java`. Applications built on frameworks that do not support the module path may skip it, with a note in the README.
- `JAVA-15.2` [MUST] name the Java module after its root package: `module com.example.project.billing`.
- `JAVA-15.3` [MUST] export only packages that other modules are meant to use. Put implementation details in an unexported package such as `com.example.project.billing.internal`.
- `JAVA-15.4` [SHOULD] use `opens` only for packages a framework must reach by reflection (JSON mapping, dependency injection), and prefer `opens ... to <module>` over opening to everyone.
- `JAVA-15.5` [SHOULD] use `requires transitive` only for modules whose types appear in your exported API.
- `JAVA-15.6` [MUST] write `requires` modifiers in the order `transitive`, then `static`.
- `JAVA-15.7` [MUST NOT] create split packages (the same package in two modules).
- `JAVA-15.8` [MUST NOT] use module import declarations (`import module java.base;`); they are a preview feature in Java 23.
- `JAVA-15.9` [SHOULD] mark each package `@NullMarked` in `package-info.java`, alongside its package doc comment (section 10).

```java
/// Billing domain: invoices, payments and credit notes.
module com.example.project.billing {
  requires transitive com.example.project.money;
  requires org.slf4j;
  requires static org.jspecify;

  exports com.example.project.billing;
  opens com.example.project.billing.json to com.fasterxml.jackson.databind;
}
```

## 16. Logging and diagnostics

Libraries log through the SLF4J API only; the application chooses the backend. Log messages are parameterized, levelled consistently, and never contain secrets.

| Level | Use for | Enabled in production |
| --- | --- | --- |
| `ERROR` | A failure that needs a person: a request failed, data could not be saved | Yes |
| `WARN` | Something unexpected the system recovered from: a retry, a fallback, a deprecated call | Yes |
| `INFO` | Lifecycle and business events: startup, shutdown, an invoice issued | Yes |
| `DEBUG` | Detail for diagnosing a specific problem | Off by default, on per package |
| `TRACE` | Very fine-grained flow, payloads | Never in production |

**Rules**

- `JAVA-16.1` [MUST] use the SLF4J API (`org.slf4j.Logger`) in library code, never a backend directly, `System.out`, `System.err` or `printStackTrace()`.
- `JAVA-16.2` [MUST] declare the logger as `private static final Logger log = LoggerFactory.getLogger(InvoiceService.class);`. It is not a constant, so it is not UPPER_SNAKE_CASE.
- `JAVA-16.3` [MUST] use `{}` placeholders, never string concatenation: `log.info("Issued invoice {} for {}", id, customerId);`.
- `JAVA-16.4` [MUST] pass the exception as the last argument so the stack trace is kept: `log.error("Could not send invoice {}", id, e);`.
- `JAVA-16.5` [MUST NOT] log passwords, tokens, API keys, full card numbers or personal data, at any level.
- `JAVA-16.6` [SHOULD] put request-scoped context (request ID, tenant) in the MDC rather than repeating it in every message, and clear it when the request ends.
- `JAVA-16.7` [SHOULD] log state changes and decisions, not method entry and exit.
- `JAVA-16.8` [SHOULD] emit JSON logs from services so they can be searched by field.

**Diagnostics**

- `JAVA-16.9` [SHOULD] keep Java Flight Recorder available in production (`-XX:StartFlightRecording` with a bounded, rolling recording) and add custom `jdk.jfr.Event`s around key operations.
- `JAVA-16.10` [SHOULD] expose health and metrics through the application framework (Micrometer or equivalent) rather than through logs.

## 17. Testing

Tests use JUnit 5 and AssertJ, live in the same package as the code they test under `src/test/java`, and are fast and deterministic. Integration tests that need real infrastructure live in their own module and run against Testcontainers.

**Layout and naming**

- `JAVA-17.1` [MUST] name a test class after the class it tests plus `Test`, in the same package: `com.example.project.billing.InvoiceServiceTest`.
- `JAVA-17.2` [SHOULD] name test methods as behavior: `methodUnderTest_condition_expectedResult` or a plain sentence in lowerCamelCase. Underscores between parts are allowed in test names only.
- `JAVA-17.3` [SHOULD] give each test the shape arrange, act, assert, separated by blank lines.
- `JAVA-17.4` [MUST] keep unit tests in `src/test/java` of the module they test, and tests that start containers, servers or databases in the `project-it` module (run by the Failsafe plugin).

**Rules**

- `JAVA-17.5` [MUST] write assertions with AssertJ (`assertThat(...)`), not JUnit's `assertEquals`, for readable failures.
- `JAVA-17.6` [MUST] add a failing test that reproduces a bug before fixing it.
- `JAVA-17.7` [SHOULD] test through the public (or package-private) API. A test that needs reflection to reach a private member is a sign the class wants splitting.
- `JAVA-17.8` [MUST] make tests deterministic: inject a fixed `Clock`, seed any randomness, and never sleep to wait; poll with Awaitility if you must wait.
- `JAVA-17.9` [SHOULD] use `@ParameterizedTest` for the same behavior over several inputs instead of copying the test.
- `JAVA-17.10` [SHOULD] prefer real objects and simple in-memory fakes over mocks. Use Mockito for awkward collaborators (HTTP clients, clocks you cannot inject), and do not mock types you own just because you can.
- `JAVA-17.11` [MUST NOT] share mutable state between tests; each test sets up what it needs.
- `JAVA-17.12` [SHOULD] keep the unit test suite of a module under about 10 seconds, so it runs on every save.

```java
class InvoiceServiceTest {
  private final Clock clock = Clock.fixed(Instant.parse("2026-10-19T12:00:00Z"), ZoneOffset.UTC);
  private final InMemoryInvoiceRepository repository = new InMemoryInvoiceRepository();
  private final InvoiceService service = new InvoiceService(repository, clock);

  @Test
  void issue_setsDueDateFourteenDaysOut() {
    Order order = Orders.withOneLine("Widget", Money.of("19.99", EUR));

    Invoice invoice = service.issue(order);

    assertThat(invoice.dueDate()).isEqualTo(LocalDate.parse("2026-11-02"));
    assertThat(repository.findById(invoice.id())).contains(invoice);
  }

  @ParameterizedTest
  @ValueSource(strings = {"", " ", "\t"})
  void issue_rejectsBlankReference(String reference) {
    assertThatThrownBy(() -> service.issue(Orders.withReference(reference)))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining("reference");
  }
}
```

## 18. Documentation

Doc comments use the Markdown form (`///`) that became final in Java 23, describe what a member is for and how to use it, and start with a short summary fragment. Implementation comments explain why, never what.

### Doc comments

- `JAVA-18.1` [MUST] write new doc comments with `///` Markdown syntax. Existing `/** */` comments may stay until the file is next edited substantially.
- `JAVA-18.2` [MUST] document every public type and every public or protected member of a public type. Skip it only when there is truly nothing to add (a trivial getter) and for `@Override` methods that keep the parent's contract.
- `JAVA-18.3` [MUST] start with a summary fragment: a noun or verb phrase, capitalized and ending with a period, not a full sentence. Write "Returns the invoice total.", not "This method returns the invoice total."
- `JAVA-18.4` [SHOULD] say what callers need: valid inputs, what is returned when nothing is found, which exceptions are thrown and when, thread safety, and whether the method keeps references to its arguments.
- `JAVA-18.5` [MUST] order block tags `@param`, `@return`, `@throws`, `@deprecated`, and never leave a tag's description empty.
- `JAVA-18.6` [SHOULD] separate paragraphs with a blank `///` line and use Markdown for code (`` `name` ``), lists and links (`[Invoice]`, `[Invoice#total()]`).

```java
/// Issues an invoice for a completed order and stores it.
///
/// The due date is fourteen days after today, using the injected clock.
/// Issuing the same order twice creates two invoices; callers that need
/// idempotency must check [InvoiceRepository#findByOrder(OrderId)] first.
///
/// @param order the completed order; must have at least one line
/// @return the stored invoice, in [InvoiceStatus#ISSUED] state
/// @throws IllegalArgumentException if the order has no lines
public Invoice issue(Order order) { ... }
```

### Implementation comments

- `JAVA-18.7` [MUST] write implementation comments with `//`. Block comments (`/* */`) are allowed for long notes; align their `*` and never draw boxes.
- `JAVA-18.8` [MUST] explain why when the code does something surprising: a workaround, a performance trick, an ordering requirement. Leave the what to the code.
- `JAVA-18.9` [MUST] write TODOs as `// TODO: <issue link> - <what needs doing>`. A TODO that depends on time names a date or event: `// TODO: #318 - remove after the 2027-01 API migration`.
- `JAVA-18.10` [SHOULD] use `// NOTE:` for surprising but correct code and `// HACK:` for knowingly wrong code with a plan to remove it.
- `JAVA-18.11` [AVOID] commented-out code. Git remembers it.
- `JAVA-18.12` [SHOULD] record significant decisions (a framework choice, a persistence model, a concurrency approach) as short ADRs in `docs/adr/NNNN-title.md`.

## 19. Build and static analysis

Maven builds every project through the committed wrapper, and `./mvnw verify` runs formatting checks, compilation with Error Prone and NullAway, unit tests and integration tests. A green `verify` means the change is ready for review.

**What `verify` runs**

| Step | Tool | Fails the build when |
| --- | --- | --- |
| **Enforce environment** | maven-enforcer-plugin | JDK is not 23, Maven too old, dependency versions conflict |
| **Check formatting** | Spotless + google-java-format | Any file is not formatted |
| **Compile** | maven-compiler-plugin, `--release 23`, `-Xlint:all -Werror` | Any compiler warning |
| **Bug patterns** | Error Prone | Any error-level finding |
| **Null safety** | NullAway (JSpecify mode) | Any possible null dereference |
| **Unit tests** | Surefire + JUnit 5 | Any test fails |
| **Integration tests** | Failsafe + Testcontainers (`project-it`) | Any test fails |

**Rules**

- `JAVA-19.1` [MUST] set every plugin and dependency version in the parent POM's `<pluginManagement>` and `<dependencyManagement>`, importing BOMs (JUnit, Jackson, Testcontainers) where they exist. Child modules never state versions.
- `JAVA-19.2` [MUST] run `./mvnw spotless:apply` before committing (or install the pre-commit hook); CI only runs `spotless:check`.
- `JAVA-19.3` [MUST] treat warnings as errors in CI. A warning that cannot be fixed is suppressed at the narrowest scope with a comment.
- `JAVA-19.4` [SHOULD] enable reproducible builds by setting `project.build.outputTimestamp`.
- `JAVA-19.5` [SHOULD] let Dependabot or Renovate propose dependency updates weekly, merged as their own commits.
- `JAVA-19.6` [MUST NOT] add a dependency for something the JDK already does well (`java.net.http.HttpClient`, `java.time`, `java.util.concurrent`) without a reason in the commit message.
- `JAVA-19.7` [SHOULD] use Gradle (Kotlin DSL) only where a project already depends on it; the same checks apply.

The full parent POM is in appendix A.

## 20. Performance

Write the clear version first, measure with JFR or JMH, then optimize the hot spot the measurement found, with a comment saying what it showed. The JIT handles most micro-optimizations better than hand-tuning.

**Measuring**

- `JAVA-20.1` [MUST] back any performance claim in a commit or review with a measurement: a JMH benchmark, a JFR recording or a load-test result, with before and after numbers.
- `JAVA-20.2` [SHOULD] write micro-benchmarks with JMH in a separate `project-bench` module, never with `System.nanoTime()` loops in tests.
- `JAVA-20.3` [SHOULD] profile production-like load with JFR and JDK Mission Control before changing code.

**Common costs worth knowing**

- `JAVA-20.4` [SHOULD] avoid autoboxing in hot loops: use `int` over `Integer`, `IntStream` over `Stream<Integer>`, and primitive-specialized collections where a profile shows boxing matters.
- `JAVA-20.5` [SHOULD] build strings in loops with `StringBuilder`; simple concatenation outside loops is fine (the compiler optimizes it).
- `JAVA-20.6` [SHOULD] pre-size `ArrayList`, `HashMap` and `StringBuilder` when the size is known.
- `JAVA-20.7` [MUST] compile a `Pattern` once into a `static final` field rather than calling `String.matches` or `Pattern.compile` per call in hot code.
- `JAVA-20.8` [AVOID] exceptions for control flow on hot paths; filling in stack traces is expensive.
- `JAVA-20.9` [SHOULD] use streaming APIs for large inputs (files, query results) instead of loading everything into memory.

**JVM settings**

- `JAVA-20.10` [SHOULD] use Generational ZGC (the default ZGC mode in Java 23: `-XX:+UseZGC`) for latency-sensitive services, and the default G1 for batch and throughput work, unless measurement says otherwise.
- `JAVA-20.11` [SHOULD] set an explicit maximum heap (`-Xmx` or `-XX:MaxRAMPercentage` in containers) and record the choice next to the deployment config.
- `JAVA-20.12` [MUST NOT] use `sun.misc.Unsafe` memory-access methods (deprecated for removal in Java 23). Use `VarHandle` or the Foreign Function & Memory API.

## 21. Git and review

Commits are small, focused and written in Conventional Commits form. One commit does one thing, builds on its own, and can be reverted on its own.

```text
<type>(<scope>): <imperative summary, under 72 characters>

<optional body: why the change was needed, what it trades off>
```

| Type | Use for | Example |
| --- | --- | --- |
| `feat` | New capability | `feat(billing): add credit notes` |
| `fix` | Bug fix | `fix(billing): round VAT per line, not per invoice` |
| `perf` | Measured speedup | `perf(export): stream rows, 2.1 GB to 180 MB heap` |
| `refactor` | No behavior change | `refactor(shipping): extract RateCalculator` |
| `test` | Tests only | `test(billing): cover zero-amount invoices` |
| `build` | Maven, plugins, dependencies | `build: bump junit-bom` |
| `style` | Formatter runs, renames with no logic change | `style: apply google-java-format` |
| `docs` | Docs and ADRs | `docs(adr): choose jOOQ over JPA` |
| `chore` | Everything else | `chore: ignore .idea` |

- `JAVA-21.1` [MUST] use the feature package or module as the scope (`billing`, `shipping`, `api`).
- `JAVA-21.2` [MUST] keep formatting-only and rename-only changes in their own commits so real diffs stay readable.
- `JAVA-21.3` [SHOULD] mark breaking API changes with `!` and a `BREAKING CHANGE:` footer.

**Review checklist**

- [ ] `./mvnw verify` passes: format, compile with no warnings, Error Prone, NullAway, tests
- [ ] Names follow section 5; no abbreviations by deleted letters
- [ ] New types are `final`, immutable or records where they can be
- [ ] Nullable values are marked `@Nullable`; `Optional` only as a return type
- [ ] No swallowed exceptions; causes kept when translating
- [ ] No preview features in production code
- [ ] Shared mutable state is guarded and its thread safety documented
- [ ] Public types and members have `///` doc comments
- [ ] New behavior has tests; a bug fix has a regression test
- [ ] Commit messages are Conventional Commits

## Appendix

Drop-in files for a new repository. Replace `com.example.project` and `project` with your own names, and set each `*.version` property to the current release of that tool (Renovate or Dependabot keeps them current afterwards).

### A. Parent pom.xml

```xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example.project</groupId>
  <artifactId>project-parent</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <modules>
    <module>project-core</module>
    <module>project-api</module>
    <module>project-it</module>
  </modules>

  <properties>
    <maven.compiler.release>23</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.outputTimestamp>2026-01-01T00:00:00Z</project.build.outputTimestamp>

    <!-- Pin each to the current release. -->
    <errorprone.version>CURRENT</errorprone.version>
    <nullaway.version>CURRENT</nullaway.version>
    <jspecify.version>CURRENT</jspecify.version>
    <junit.version>CURRENT</junit.version>
    <assertj.version>CURRENT</assertj.version>
    <spotless.version>CURRENT</spotless.version>
    <google-java-format.version>CURRENT</google-java-format.version>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.junit</groupId>
        <artifactId>junit-bom</artifactId>
        <version>${junit.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.jspecify</groupId>
        <artifactId>jspecify</artifactId>
        <version>${jspecify.version}</version>
      </dependency>
      <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>${assertj.version}</version>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <configuration>
            <release>23</release>
            <compilerArgs>
              <arg>-Xlint:all</arg>
              <arg>-Werror</arg>
              <arg>-parameters</arg>
              <arg>-XDcompilePolicy=simple</arg>
              <arg>--should-stop=ifError=FLOW</arg>
              <arg>-Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:JSpecifyMode=true</arg>
            </compilerArgs>
            <annotationProcessorPaths>
              <path>
                <groupId>com.google.errorprone</groupId>
                <artifactId>error_prone_core</artifactId>
                <version>${errorprone.version}</version>
              </path>
              <path>
                <groupId>com.uber.nullaway</groupId>
                <artifactId>nullaway</artifactId>
                <version>${nullaway.version}</version>
              </path>
            </annotationProcessorPaths>
          </configuration>
        </plugin>
        <plugin>
          <groupId>com.diffplug.spotless</groupId>
          <artifactId>spotless-maven-plugin</artifactId>
          <version>${spotless.version}</version>
          <configuration>
            <java>
              <googleJavaFormat>
                <version>${google-java-format.version}</version>
              </googleJavaFormat>
            </java>
          </configuration>
          <executions>
            <execution>
              <goals>
                <goal>check</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </pluginManagement>

    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-enforcer-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>enforce</goal>
            </goals>
            <configuration>
              <rules>
                <requireJavaVersion>
                  <version>[23,24)</version>
                </requireJavaVersion>
                <dependencyConvergence/>
              </rules>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>com.diffplug.spotless</groupId>
        <artifactId>spotless-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
```

Error Prone needs access to compiler internals on modern JDKs. Add this `.mvn/jvm.config` beside the POM:

```text
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
```

### B. .editorconfig

```ini
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

[*.java]
max_line_length = 100

[*.{xml,yml,yaml,json}]
indent_size = 2

[*.md]
trim_trailing_whitespace = false
```

### C. package-info.java

```java
/// Invoices, payments and credit notes.
///
/// Every type in this package is non-null by default; nullable values are
/// marked with [org.jspecify.annotations.Nullable].
@NullMarked
package com.example.project.billing;

import org.jspecify.annotations.NullMarked;
```

### D. Starting a new project

1. Copy the parent POM, `.mvn/jvm.config` and `.editorconfig`; replace `com.example.project` and `project`.
2. Pin the JDK in `.java-version` and in CI, and commit the Maven wrapper.
3. Set every `*.version` property to the current release.
4. Add `package-info.java` with `@NullMarked` to the first package.
5. Run `./mvnw verify` and confirm a clean build with no warnings.
6. Write `docs/adr/0001-project-scope.md` saying what the project is and is not.
