Adopt JSpecify nullness annotations for compile-time null safety

Context and Problem Statement

Our Java codebase contains inconsistent handling of nullability. We want to detect null-safety issues earlier at compile time and improve API clarity.

Decision Drivers

  • Reduce NullPointerExceptions in production by shifting detection to compile time.
  • Provide clearer API contracts.
  • Keep runtime overhead low.
  • Ensure incremental and low-risk adoption.

Considered Options

  • Do nothing and keep status quo.
  • Use Objects.requireNonNull and defensive runtime checks.
  • Adopt JSpecify annotations.
  • Use assert x == null.

Decision Outcome

Chosen option: “Adopt JSpecify annotations” because it comes out best (see below).

Consequences

  • Earlier detection of potential NPEs.
  • Clearer API documentation.
  • Requires training and CI integration.
  • Some friction with unannotated third-party libraries.

Pros and Cons of the Options

Do nothing and keep status quo

  • Good, because no immediate implementation work.
  • Bad, because NPEs remain runtime-only problems.
  • Bad, because a mix of different approaches leads to bad code.
  • Bad, because it increases technical debt.
  • Bad, because assumes that non-annotated symbols allow null.

Use Objects::requireNonNull

  • Good, because JDK native.
  • Good, because no external dependencies.
  • Good, because it makes NPEs more visible on runtime.
  • Bad, because it adds runtime overhead.
  • Bad, because it does not provide compile-time contracts.
  • Bad, because it is not self-documenting for API contract.

Adopt JSpecify annotations

  • Good, because it offers compile-time null safety detection.
  • Good, because it works well with common IDEs.
  • Good, because of standardized annotations.
  • Good, because incremental adoption possible.
  • Good, because static analysis supported.
  • Good, because compatibility with Kotlin.
  • Good, because it is the consensus among major organizations (Google, Microsoft, Jetbrains etc).
  • Bad, because it requires annotation effort.
  • Bad, because it requires developer training.

Use assert x == null

  • Good, because Java language native.
  • Good, because no external dependencies.
  • Good, because easily readable.
  • Good, because it makes NPEs more visible on runtime.
  • Bad, because runs by default only in debug mode with option “-ea”.
  • Bad, because it does not provide compile-time null safety.
  • Bad, because it is not self-documenting for API contract.