Follow this repo's Maven rules — versions only in the root pom, module poms version-free, the build profiles, and clean-after-codegen. Use when editing a pom.xml, adding a dependency or plugin, choosing a build command, or when the user says "add a dependency", "bump a version", or "the build fails".
Installs into .claude/skills of the current project.
Are you the author of Maven Conventions?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/adamw7-maven-conventions)
---
name: maven-conventions
description: Follow this repo's Maven rules — versions only in the root pom, module poms version-free, the build profiles, and clean-after-codegen. Use when editing a pom.xml, adding a dependency or plugin, choosing a build command, or when the user says "add a dependency", "bump a version", or "the build fails".
---
# Maven Conventions Skill
Keep `pom.xml` edits and build commands consistent with how the `tools`
multi-module reactor is wired. Getting versions in the wrong place, or missing a
profile, is the most common source of avoidable build friction here.
## Hard rules
### Versions live in exactly one place
- **Dependency versions and scopes**: only in the **root** `pom.xml` under
`<dependencyManagement>`. A module may say one thing about a managed
dependency — a *narrower* scope, where two modules genuinely want different
ones (`enforcer-api` is managed `provided` and narrowed to `test` by `adopt`;
`jsoup` is managed unscoped and narrowed to `test` by `claude-code-enforcer`).
- **Plugin versions**: only in the **root** `pom.xml` under
`<pluginManagement>`.
- **Module poms reference dependencies and plugins WITHOUT versions.** Never add
a `<version>` to a module pom — add or change it in the root instead. The
exceptions are reactor artifacts, which use `${project.version}`.
- **Artifacts that must move together share one property**, not a version each:
`protobuf.version` (runtime *and* `protoc`), `grpc.version`, `derby.version`,
`log4j2.version`, `maven.api.version`. `derby.version` and `log4j2.version`
are Spring Boot's own property names, so overriding them also moves the
siblings the `spring-boot-dependencies` BOM manages (derbyshared, the other
log4j2 artifacts) instead of leaving them on Boot's older version.
- **The Spring Boot parent manages plugins too.** It binds a `generate`
execution on `protobuf-maven-plugin` and adds `protoc-gen-grpc-java` to it for
its gRPC starter; the root pom unbinds that execution (`<phase>none</phase>`)
and clears the generator list (`<plugins combine.self="override"/>`), because
the modules here declare the protobuf executions they want. Leave both in
place — without them `protogen-maven-plugin` fails at `generate-sources`
looking for a `src/main/proto` it does not have.
### Ask before adding a dependency
- Use the existing Maven dependencies. **Always ask the user before adding a new
one.** If approved, declare it (with version) in root `<dependencyManagement>`,
then reference it version-free in the module.
### Name the module of every published jar
- A published jar states its JPMS module name instead of letting the JVM derive
one from the filename, which changes with the artifactId or the version scheme
and breaks a consumer's `requires`.
- `data` and `data-test` state it in `module-info.java`. Every other published
module states it in the jar manifest, via `maven-jar-plugin`:
```xml
<archive>
<manifestEntries>
<Automatic-Module-Name>tools.markdown.common</Automatic-Module-Name>
</manifestEntries>
</archive>
```
- The name is the artifactId with every non-alphanumeric run collapsed to a dot
(`tools.markdown-common` → `tools.markdown.common`), so pinning it changes
nothing for a consumer already reading the derived name.
`protogen-maven-plugin` is the exception: no `tools.` prefix in its artifactId,
and nothing `requires` a Maven plugin, so it takes `tools.protogen.maven.plugin`.
- **A new published module needs this too.** The unpublished ones
(`grpc-example`, `assembly`, `*-test`, anything with
`central.skipPublishing`) need nothing.
### Clean after removing a code-generation source
- Run `clean` after deleting a codegen input, so stale generated builders in
`target/` cannot mask the change.
## Build commands (run from the repo root)
| Command | Purpose |
|---|---|
| `mvn clean install` | Full clean build + install to local repo |
| `mvn install` | Faster incremental build |
| `mvn -pl <module> -am package` | Tests for a single module (`-am` is required — a bare `-pl` fails the `ReactorModuleConvergence` rule; `package`, since `data` needs `mcp-common`'s jar to resolve its module name) |
| `mvn -P integration-tests verify` | MCP integration tests (`*IT`) |
| `mvn -Pcoverage verify` | JaCoCo coverage (fails under 80% instruction or branch) |
| `mvn -Ppitest install` | PIT mutation testing (needs a phase past `package`) |
| `mvn install -Dskip.shellcheck=true` | Skip the shellcheck lint |
## Notes that save time
- **Shellcheck runs embedded.** `dev.dimlight:shellcheck-maven-plugin` uses
`<binaryResolutionMethod>embedded</binaryResolutionMethod>` — the binary ships
in the plugin jar (from Maven Central), so the build needs no installed
`shellcheck` and works offline. Skip it with `-Dskip.shellcheck=true` when you
just want a fast loop.
- **Java 25** is required: JDK 25 on `PATH` with `JAVA_HOME` set.
- The root reactor modules are: `markdown-common`, `claude-code-enforcer`,
`test-common`, `mcp-common`, `data`, `code`, `adopt`, `grpc-example`,
`assembly`. `data-test` is built separately (not in the root `<modules>`).
- **`markdown-common` takes no dependencies.** It is shared by the enforcer rule
and by `adopt` precisely so both read a document identically; anything added
there travels to every consumer of either, including repositories that resolve
the enforcer rule as a maven-enforcer-plugin dependency. Its architecture test
fails the build on a dependency outside `java..`.
- **A typo in `-P` fails the build**, not the run: the root `enforce` execution
runs `requireProfileIdsExist`.
- **The `claude-md-enforce` profile activates on the root project only**, by
requiring a `CLAUDE.md` beside the pom as well as the `enforceClaudeMd`
property. A profile in a parent is inherited, so without that second condition
every module takes on the profile's `maven-enforcer-plugin` declaration — and
Maven orders a project after every plugin dependency it declares, giving each
module a reactor edge to `claude-code-enforcer`. Maven quietly tolerates a
cycle closed by a plugin edge, so that stayed invisible until a reactor module
became a real dependency of the enforcer (`markdown-common`); the cycle then
closed through a dependency edge and `mvn -B package -DenforceClaudeMd` could
not sort the reactor at all. **Do not put a reactor artifact in a plugin's
`<dependencies>` in an inherited profile.**
- **A module that is not a reusable library** (example, test harness,
distribution) opts out of publishing with `<maven.deploy.skip>` and
`<central.skipPublishing>` in its `<properties>` — never by redeclaring the
`release` profile.
## Workflow for a dependency/plugin change
1. Confirm it isn't already managed in the root pom.
2. If it's a *new* dependency, **ask the user first**.
3. Add/adjust the version in root `<dependencyManagement>` or
`<pluginManagement>`.
4. Reference it version-free in the module pom.
5. Build: `mvn install` (add `clean` if you removed a codegen source).
## References
- `CLAUDE.md` / `AGENTS.md` — *Maven* section (source of truth)
- Root `pom.xml` — `<dependencyManagement>`, `<pluginManagement>`