Maven parent POM for Java projects by Open Elements.
Inherit from this POM to get a consistent, modern Java build out of the box: pinned plugin versions, managed dependency BOMs, code formatting, an SBOM, and a complete publish-to-Maven-Central release pipeline — without copying boilerplate into every project.
<parent>
<groupId>com.open-elements</groupId>
<artifactId>java-parent</artifactId>
<version>1.0.0</version>
</parent>Released artifacts are published to Maven Central;
-SNAPSHOT builds are published to the Central Portal snapshot repository.
| Tool | Version | Enforced by |
|---|---|---|
| Java | 21 | maven-enforcer-plugin (requireJavaVersion) |
| Maven | 3.9.11+ | maven-enforcer-plugin (requireMavenVersion) + wrapper |
The build fails fast in the validate phase if the local toolchain does not
meet these requirements. Both minimums can be raised by a child project by
overriding the enforcer.requiredJavaVersion / enforcer.requiredMavenVersion
properties.
A Maven Wrapper (./mvnw) pinned to 3.9.11 is included, so no local Maven
installation is required.
Import-scoped BOMs so child projects can declare these dependencies without a
<version>:
- Spring Boot —
spring-boot-dependencies(3.5.14) - Testcontainers —
testcontainers-bom(2.0.5) - OpenAPI —
springdoc-openapi-bom(2.8.17) andswagger-bom(2.2.47), plusorg.webjars:swagger-ui(5.32.2) — see The OpenAPI stack
springdoc, Swagger and the Swagger UI webjar are one coupled set, and the parent manages all three so a child gets a version-uniform stack without configuring anything.
This is not tidiness. springdoc declares its Swagger dependency without a version and
inherits it from its own aggregator POM, so Swagger floats in every consuming project
and is decided by nearest-wins mediation. A library that uses only the Swagger
annotations declares only that artifact — the normal thing to do — and its
declaration sits closer to the application than the swagger-core-jakarta springdoc
contributes two levels down. The stack then splits along exactly that boundary, and
Swagger's own modules call each other across incompatible releases:
io.swagger.v3.core.jackson.ModelResolver (swagger-core-jakarta 2.2.47)
→ io.swagger.v3.oas.annotations.media.Schema.$dynamicRef()
absent in annotations 2.2.29
The result is a NoSuchMethodError at schema resolution, not a missing feature.
Managing the versions in the parent removes the mediation entirely, because
dependencyManagement is consulted before nearest-wins and applies at any depth.
What this does not cover. Management overrides transitive resolution only. A
project that declares a Swagger artifact directly with an explicit <version> still
wins over the managed version and keeps whatever it had. Dropping that <version>
activates the managed one.
Retargeting the stack in a child. Setting <swagger.version> in a child moves all
14 Swagger coordinates at once. The floor is 2.2.47: swagger-bom was first
published at that version, so a lower value fails the build with a non-resolvable
import. Use a direct declaration with a <version> to pull an older Swagger.
springdoc.version, swagger.version and swagger-ui.version must move
together. Bumping springdoc alone would publish a split stack to every child project
at once, and this parent's own build would not notice — it has no Java sources and
never exercises the stack.
The matching values are not exposed by springdoc-openapi-bom, which manages
springdoc artifacts only. Read them from springdoc's aggregator POM:
org/springdoc/springdoc-openapi/<version>/springdoc-openapi-<version>.pom
<swagger-api.version> → swagger.version
<swagger-ui.version> → swagger-ui.version
For reference: springdoc 2.8.6 pairs with Swagger 2.2.29 and Swagger UI 5.20.1;
springdoc 2.8.17 pairs with 2.2.47 and 5.32.2.
All common build plugins are version-managed in <pluginManagement>, so child
builds are reproducible and free of "you should pin this plugin" warnings:
maven-resources, maven-compiler, maven-surefire, maven-javadoc,
maven-source, maven-gpg, maven-jar, maven-deploy, maven-clean,
maven-enforcer, cyclonedx, jreleaser, versions, spotless.
- Java 21, source/target via
maven.compiler.*. - UTF-8 for sources and reporting.
-parameterscompiler flag (parameter names retained — useful for frameworks like Spring and Jackson).- Reproducible builds via an inherited
project.build.outputTimestamp(see Reproducible builds). - Code formatting via Spotless using Google Java Format.
- Surefire pre-configured with the
--add-opensflags commonly needed by reflection-based test/mocking libraries. - Toolchain enforcement (see Requirements).
- LF line endings enforced for the formatter, so
spotless:applynever writes CRLF (see Line endings).
This repository pins line endings, and it does so for a concrete reason: with
packaging=pom and no flatten-maven-plugin, its pom.xml is deployed to Maven
Central verbatim. Checked out on a machine with core.autocrlf=true — the
Windows default — every text file arrives with CRLF, and the published .pom would
differ from one built elsewhere. For a project with Java sources the same variance
lands in the sources jar and in every copied resource.
.gitattributes forces LF in the working tree on every platform, overriding whatever
core.autocrlf or core.eol the developer has set. Windows batch files are the
single exception, because cmd.exe is fragile with LF-only scripts. .editorconfig
mirrors the same rules for editors.
This eliminates line endings as a source of byte-level variance. It does not by itself make builds reproducible across operating systems — that involves further variables and is not claimed here.
Git attributes are repository-local: Maven inheritance cannot deliver them. A child
project inherits the Spotless setting below, but must carry its own .gitattributes.
Copy this file into the repository root:
# Force LF in the working tree on every platform, overriding local core.autocrlf.
* text=auto eol=lf
# Windows batch files are the exception: cmd.exe is fragile with LF-only scripts.
*.bat text eol=crlf
*.cmd text eol=crlf
# Truly binary files: never normalized, never diffed as text.
*.class binary
*.eot binary
*.gif binary
*.gz binary
*.ico binary
*.jar binary
*.jks binary
*.jpeg binary
*.jpg binary
*.p12 binary
*.pdf binary
*.png binary
*.ttf binary
*.woff binary
*.woff2 binary
*.zip binaryThen run git add --renormalize . once and commit whatever it stages. In a
repository that never received CRLF this is a no-op.
Take this project's .editorconfig along with it. Its [*.java]
block is set to 2 spaces and a 100 column limit, matching the Google Java Format
this parent enforces via Spotless — a .editorconfig specifying anything else makes
the editor fight the formatter on every save. The [*.{cmd,bat}] block keeps editors
in agreement with the CRLF pin above.
The parent sets <lineEndings>UNIX</lineEndings> on the Spotless plugin, which every
child inherits. Spotless defaults to GIT_ATTRIBUTES, so without this a
spotless:apply run on a Windows machine in a repository lacking .gitattributes
would actively write CRLF into Java sources.
The reach of that setting is narrow, and worth stating plainly: it applies only to
files Spotless formats, and only when spotless:apply or spotless:check is invoked
— neither is bound to a lifecycle phase. It guarantees the mandated formatter never
introduces CRLF. It does not make a child's sources jar LF-clean; only the child's
own .gitattributes does that.
Building the same source twice yields byte-identical artifacts — jar, sources jar, javadoc jar and the CycloneDX SBOM.
This works because java-parent declares a fixed timestamp that every child project
inherits:
<properties>
<project.build.outputTimestamp>2026-08-28T00:00:00Z</project.build.outputTimestamp>
</properties>Child projects set nothing. They inherit the value by pinning a java-parent
version, and release.sh updates it whenever a new java-parent release is cut.
The same source, built with the same toolchain, produces byte-identical artifacts — regardless of when or where the build runs.
"Same toolchain" is part of the promise, not a footnote. Reproducibility across differing JDK patch versions, Maven versions, operating systems or locales has not been measured and is not claimed. Javadoc output in particular is known to vary between JDK builds. Use the Java and Maven versions this project enforces.
No build flags and no insider knowledge are needed. Apart from its deployment arguments, this is the same command CI runs:
git checkout vA.B.C # any release that carries the timestamp property
./mvnw -Pfull-build clean verifyThen compare the result against the artifacts published on Maven Central, for example
with sha256sum. They must match.
Do not pass
-Dproject.build.outputTimestamp, and do not override the property in a child POM. Both take precedence over the inherited value (-Dbeats a child POM, which beats the parent), so either one silently produces artifacts that nobody else can reproduce.
project.build.outputTimestamp records which java-parent release an artifact was
built against. It is deliberately not the time the build ran — a real build time
cannot be reproduced, which is the whole point.
If an application needs to answer "which state is this?", use the Git metadata that
the full-build profile writes into every jar's META-INF/MANIFEST.MF:
| Manifest entry | Meaning |
|---|---|
Git-Commit-Time |
When the source state came into being (UTC, commit-derived) |
Git-Commit |
Abbreviated commit id |
Git-Branch |
Branch the build came from |
Git-Tag |
Tags pointing at the commit |
Git-Dirty |
Whether the working tree had uncommitted changes |
Git-Commit-Time is the correct replacement for any buildTime / build.time field.
Such a field sourced from project.build.outputTimestamp would be misleading and must
not be introduced.
When the build runs outside a Git checkout — for example from a published source
archive — these entries are present but empty. The build deliberately does not fail
(failOnNoGitDirectory is false) so that reproducing from sources stays possible, and
the empty values are themselves deterministic.
Tracked in docs/TODO.md:
- No automated check guards reproducibility — a plugin upgrade could reintroduce non-determinism unnoticed.
- Cross-toolchain reproducibility is unmeasured — see What is promised.
- Snapshot builds are not reproducible, by decision. A project pinning a
-SNAPSHOTparent inherits a value that moves when the snapshot is republished. - A project pinning a
java-parentversion older than the first release carrying the property inherits nothing and stays non-reproducible. Raising the parent version is what actually switches this on for a downstream project.
# Build and test
./mvnw clean verify
# Apply code formatting / check formatting
./mvnw spotless:apply
./mvnw spotless:check
# Full build: also attaches Javadoc jar, sources jar and a CycloneDX SBOM
./mvnw -Pfull-build clean verify
# Check for newer dependency, plugin and property versions
./check-dependencies.shcheck-dependencies.sh runs the
versions-maven-plugin and writes three
reports to target/:
dependency-updates.txtplugin-updates.txtproperty-updates.txt
| Profile | Purpose |
|---|---|
full-build |
Attaches the Javadoc jar, sources jar, and generates a CycloneDX SBOM. Used for releases & CI. |
deploy-release |
Signs artifacts (GPG) and publishes to Maven Central + creates the GitHub release via JReleaser. |
Releases are cut with release.sh and finished by CI — the script only prepares
git state, it never deploys:
./release.sh <release-version> <next-snapshot-version>
# e.g.
./release.sh 1.1.0 1.2.0-SNAPSHOTThe script:
- Sets the release version in the POM.
- Pins
project.build.outputTimestampto the release date and verifies the rewrite took effect — a stale timestamp would publish artifacts that no rebuild could match, so the release aborts rather than continuing. - Runs
./mvnw -Pfull-build clean verifylocally — so a broken build, missing Javadoc link, or SBOM error fails here, not after the tag is pushed. Because the timestamp is already pinned, these are the bytes CI will publish. - Best-effort generates upgrade documentation under
docs/releases/(requires the Claude Code CLI; skipped with a warning if absent). - Commits version and timestamp together, tags
vA.B.C, pushes, then bumps to the next-SNAPSHOT(leaving the timestamp at the release date).
Pushing the vA.B.C tag triggers the release workflow, which verifies the POM
version matches the tag, stages artifacts, and publishes to Maven Central while
creating the GitHub release.
| Workflow | Trigger | What it does |
|---|---|---|
build.yml |
Pull requests to main |
./mvnw clean verify |
snapshot.yml |
Push to main |
Publishes -SNAPSHOT artifacts to the Central Portal snapshot repo. |
release.yml |
Push of a v*.*.* tag |
Builds, signs, and deploys to Maven Central; creates a GitHub release. |
Licensed under the Apache License, Version 2.0.