Skip to content
Deze documentatie wordt actief uitgebreid — kom regelmatig terug.

ADR-00018: Build-enforced Java formatting via Spotless + an Eclipse JDT profile

Status flow: Proposed → Accepted → Reviewed → Approved (or Rejected), or Not Required. A superseded ADR keeps its file; only its status changes to Superseded by NNNNN.

Governance bodyStatusLast update
CTN Project TeamProposed2026-06-24
CTN Technical Advisory BoardPending
CTN Steering CommitteePending
BDI Conformance TeamPending
BDI Framework TeamPending

asr-service (Quarkus) has no machine-enforced Java formatting. Sources today follow IntelliJ IDEA’s default formatting by convention only — there is no shared config, no apply command, and no CI gate. Contributors may use IntelliJ (primary), Eclipse, or VS Code, so formatting can drift per editor and per person, producing noisy diffs and review friction.

We want three properties:

  1. A single command formats the whole module (mvn spotless:apply).
  2. CI fails when any file is not formatted.
  3. All three IDEs auto-format identically to the build from one shared config.

Formatting is not linting. Spotless is a formatter: it rewrites layout — whitespace, indentation, line breaks, import order — to one canonical style. It does not judge what the code does: unused variables, likely bugs, null-safety, complexity, and security anti-patterns are all out of scope. Those are the job of a linter / static-analysis tool (Error Prone, SpotBugs, PMD, Checkstyle, SonarQube). A formatter makes code look the same; a linter checks that code is well-formed. This ADR is strictly about formatting; linting is a separate, not-yet-taken decision — see Follow-up: linting below.

The repository is a monorepo (CNCL-BDI) holding several components with deliberately separate builds: asr-service (nl.bdi.asr), visibility-dashboard/backend (nl.bdi.cloudtower), plus static/TS frontends and Keycloak assets. There is no parent reactor POM, and CI is path-scoped per subtree (see TDR-0004), whose first asr-service workflow reserves a format-gate placeholder. Any formatting decision must fit that monorepo shape rather than assume a single application.

The core board-relevant choice is the formatting engine and the style source of truth, because it determines IDE parity, CI feasibility, and the size of the one-time reformat.

Option 1: spotless-maven-plugin driving an Eclipse JDT formatter profile (chosen)

Section titled “Option 1: spotless-maven-plugin driving an Eclipse JDT formatter profile (chosen)”
  • Description: Use com.diffplug.spotless:spotless-maven-plugin with its <eclipse> step pointed at a committed eclipse-formatter.xml. Eclipse imports the XML natively; IntelliJ achieves true parity via the Adapter for Eclipse Code Formatter plugin (delegates to the real Eclipse JDT engine); VS Code reads it via java.format.settings.url.
  • Pros: One config consumed by all three IDEs; the Eclipse JDT formatter runs in-process and headless, so it is fast in CI; spotless:check binds to verify for a zero-wiring local gate.
  • Cons: The Eclipse JDT engine differs from IntelliJ’s native engine, so native IntelliJ XML import is only approximate — true parity requires the per-contributor Adapter plugin.

Option 2: googleJavaFormat / palantirJavaFormat

Section titled “Option 2: googleJavaFormat / palantirJavaFormat”
  • Description: Adopt an opinionated, fixed formatter as the Spotless step.
  • Pros: Zero style config to maintain; deterministic.
  • Cons: Opinionated styles (e.g. 2-space indent) diverge heavily from the current 4-space, IntelliJ-default code — maximal one-time churn — and still need a per-IDE plugin for live parity. Not consumable as a shared Eclipse/IntelliJ/VS Code config.
  • Description: Let Spotless invoke IntelliJ’s own formatter.
  • Cons: Requires a local IntelliJ binary and launches a separate IDEA process per file — unusable in CI and for non-IntelliJ contributors. Rejected outright.

Chosen option: Option 1 — spotless-maven-plugin with an Eclipse JDT profile — together with the following scoping and operational decisions that make it fit the monorepo:

  • Scope: asr-service only. Not visibility-dashboard/backend, not the TS frontends. This fills TDR-0004’s format-gate placeholder and avoids inventing shared formatting plumbing across builds that are deliberately separate — the same “no premature abstraction across notional consumers” reasoning TDR-0004 applied to CI. vis-dashboard can adopt the proven config later as its own installment.
  • Config location: asr-service/config/eclipse-formatter.xml, referenced via ${project.basedir} — self-contained inside the one subtree its path-scoped CI watches; no reactor / ${maven.multiModuleProjectDirectory} path tricks.
  • Profile philosophy: clean canonical, not IntelliJ-mimicry. The profile is Eclipse built-in defaults with one deliberate overridetabulation.char=space — at width 120, K&R braces, indent 4 (all of which already match the code). We explicitly do not run the laborious key-by-key tuning loop to approximate IntelliJ’s engine; once we accept a one-time reformat, fighting an engine difference the Adapter plugin already resolves buys nothing. The XML is kept complete (all org.eclipse.jdt.core.formatter.* keys) so missing keys do not silently fall back to Eclipse’s tab-based defaults.
  • Import order is normalised, not preserved: java, javax, jakarta, org, com, <others> then static last (<order>java,javax,jakarta,org,com,\#</order>). Reshuffle churn is accepted in exchange for an explicit, documented order. Wildcard imports are forbidden (<forbidWildcardImports/>; one jakarta.persistence.* is expanded), and IntelliJ’s *-collapse thresholds are raised to 999 to stop re-collapse drift.
  • IDE parity via the Adapter for Eclipse Code Formatter plugin (IntelliJ), and the Spotless eclipse formatter version is pinned to align with the plugin’s bundled engine. CI spotless:check is the authoritative gate; the plugin is a convenience so contributors rarely hit it.
  • Deferred reformat to protect in-flight branches. The 177-file reformat would force a massive rebase on every open branch, so it is split off in time (see Implementation Plan).

Option 1 is the only one that satisfies property 3 (one config, all three IDEs) while remaining CI-viable (property 2) and low-churn against the current 4-space IntelliJ-default code. The two honest costs — the per-contributor Adapter plugin and an irreducible engine difference under native import — are mitigated by making the Adapter plugin the documented path and treating CI as the source of truth: a contributor without the plugin simply gets a gate failure pointing at mvn spotless:apply, never silently-divergent code. Scoping to asr-service and keeping the config inside that subtree mirrors the established monorepo house pattern and TDR-0004’s path-scoped CI.

  • Positive: One shared config across IDEs; fast headless CI gate; explicit import order and no wildcards; the config is self-contained in the asr-service subtree; the build stays green at every commit because the gate is armed only when the tree becomes compliant.
  • Negative: True IntelliJ parity depends on a per-contributor plugin install and a version pin that must be kept aligned; a one-time 177-file reformat commit; visibility-dashboard is left ungated for now (a future installment). Repo-root IntelliJ code style applies asr-style monorepo-wide (IntelliJ code style cannot be scoped to a subtree), so editing vis-dashboard Java in IntelliJ will nudge it toward asr-style on save — accepted as harmless eventual consistency, not a contract.
  • Neutral: vis-dashboard adoption, a shared pre-push hook, and TDR-0004’s two-layer CI split each remain separate future decisions.
  1. Plumbing commit (now), on a branch off clean main, build stays green:
    • asr-service/pom.xml: Spotless plugin <configuration> only — java includes, <eclipse> step with pinned version + ${project.basedir}/config/eclipse-formatter.xml, the <importOrder>, removeUnusedImports, forbidWildcardImports, trimTrailingWhitespace, endWithNewline. No <execution> blockspotless:apply/check are runnable on demand but nothing binds to verify, so the gate does not fail on still-unformatted code.
    • asr-service/config/eclipse-formatter.xml (the canonical profile, name="asr-style").
    • Repo-root IDE files: .editorconfig (universal settings + [*.java] space/4), .vscode/settings.json (java.format.settings.urlasr-service/config/eclipse-formatter.xml, profile asr-style), .idea/codeStyles/Project.xml + codeStyleConfig.xml (un-ignore exactly these two in .gitignore).
    • asr-service/CONTRIBUTING.md: the two commands, config location, Adapter plugin + pinned version, wildcard-threshold note.
  2. Deferred reformat commit (later): once in-flight branches have merged, rebase this branch onto updated main, run mvn spotless:apply, commit the reformat together with adding the <execution> checkverify binding (gate armed exactly when the tree is compliant), then add .git-blame-ignore-revs carrying that commit’s SHA so git blame skips the mass reformat. Merge.
  3. CI wiring is owned by TDR-0004: this ADR delivers pom + config + baseline only; TDR-0004’s format job calls mvn spotless:check, path-scoped to asr-service/**.
  • mvn spotless:apply is idempotent (re-run produces no changes).
  • After the reformat commit, mvn spotless:check passes on the committed tree.
  • A deliberate mis-format makes mvn verify fail with a Spotless violation pointing to apply.
  • IntelliJ “Reformat Code” (Adapter plugin, pinned version) round-trips without re-dirtying files.
  • Wildcard imports do not reappear after IntelliJ optimize-imports.
  • VS Code format-on-save round-trips cleanly against the build.

Follow-up: linting (separate, not-yet-taken decision)

Section titled “Follow-up: linting (separate, not-yet-taken decision)”

Formatting (this ADR) and linting are complementary and both are cheap to enforce in the same path-scoped CI, so linting is a natural next installment. It is not decided here; the recommendation below is a starting point for that decision.

Recommended direction, mirroring this ADR’s philosophy (build-enforced, asr-service-scoped, low-churn, introduced non-gating then armed once the baseline is clean, CI job owned by TDR-0004):

  1. Error Prone as the primary linter. A javac plugin that catches real bugs at compile time (null dereferences, format-string mistakes, broken equals/hashCode, mutable-static leaks) with famously low false positives and near-zero runtime cost. Optionally pair it with NullAway for null-safety. Best value for the effort — gate on a curated ruleset.
  2. SpotBugs + FindSecBugs as a security pass. Bytecode analysis for bug and security patterns. Because the ASR handles authentication, tokens, and PII, a security-focused linter earns its place. Start report-only, then gate on high-severity findings.
  3. Skip Checkstyle for formatting. Spotless already owns layout, so Checkstyle would overlap and add noise. Only reconsider it later for non-format conventions (naming, Javadoc presence) if a concrete need appears.
  4. Treat SonarQube / SonarCloud as a later “quality dashboard” decision. It aggregates linters and adds a quality gate plus security hotspots, but brings infrastructure. Worthwhile once there is appetite for a dashboard — not the first step.

Rollout should follow the same pattern as the Spotless gate: introduce the tool non-gating, fix the baseline in a dedicated commit (with .git-blame-ignore-revs if it touches many files), then arm the CI gate exactly when the tree is clean. Config stays inside the asr-service subtree; the CI job is owned by TDR-0004. When taken, this warrants its own ADR.

  • TDR-0004 — asr-service CI is a single path-scoped workflow; owns the format job that runs this gate.
  • ADR-00002 — preferred technology stack (Spotless is a candidate addition to its build-tooling row).
  • ADR-00009 — layered source structure / monorepo shape.
  • Handoff: .scratch/spotless-formatting-handoff.md (original plan; this ADR amends its single-module and minimal-churn-mimicry assumptions).
DateStatusNotes
2026-06-24ProposedInitial proposal

ADR format based on Michael Nygard’s template with CTN-specific enhancements

In samenwerking met

Connected Trade NetworkConclusionData in LogisticsContargoInland Terminals GroupVan Berkel