Versioning a Multi-Module Maven SDK Without Breaking CI
This is Part 1 of a series: Maven Patterns from Building an Internal SDK.
|
The Problem: Version Sprawl
Every multi-module Maven project eventually hits this: you need to release version 2.1.0, and that means updating version strings in 15, 30, or 50 pom.xml files.
You’ve probably tried the obvious solutions:
Hardcoded versions everywhere: Works until you forget one module during a release. Now you have
sdk-core:2.1.0depending onsdk-common:2.0.0and production is broken.Using
${project.version}withdependencyManagement: Works, but still requires updating the parent version manually, which cascades to every module.Maven Release Plugin: Automates the version updates, but adds complexity and couples your release process to a specific workflow.
None of these solve the fundamental problem: version strings are duplicated across files, and duplication means drift.
The Pattern: CI-Friendly Versioning
The solution is CI-friendly versioning using the ${revision} property with flatten-maven-plugin. This is a common pattern in large multi-module builds, and it eliminates the version sprawl problem entirely.
| This pattern requires Maven 3.5.0 or later. In practice, most teams standardize on Maven 3.6.x or newer to avoid resolver edge cases. |
How It Works
Define your version once in the root pom.xml:
<project>
<groupId>com.example</groupId>
<artifactId>my-sdk-parent</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<properties>
<revision>2.1.0-SNAPSHOT</revision>
</properties>
<modules>
<module>sdk-common</module>
<module>sdk-auth</module>
<module>sdk-payment</module>
</modules>
</project>Every child module references the same property:
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>my-sdk-parent</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>sdk-common</artifactId>
</project>For sibling dependencies, use ${project.version} which inherits from the parent:
<dependency>
<groupId>com.example</groupId>
<artifactId>sdk-common</artifactId>
<version>${project.version}</version>
</dependency>One property. One place to change. Every module stays in sync.
The Problem with Publishing
There’s a catch: if you publish these POMs to a repository as-is, consumers will see ${revision} in your dependency versions. Their builds will fail because they don’t have that property defined.
This is where flatten-maven-plugin comes in. It rewrites your POMs during the build, replacing ${revision} with the actual value before publishing:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.7.3</version>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>The resolveCiFriendliesOnly mode is intentional—it only resolves the three CI-friendly properties (${revision}, ${sha1}, ${changelist}) and leaves everything else untouched.
The exact execution phase is flexible; the key requirement is that flatten runs before deploy so published POMs are resolved.
Making It Work in Practice
CI/CD Integration
Override the version at build time:
# Snapshot build
mvn clean install -Drevision=2.1.0-SNAPSHOT
# Release build
mvn clean deploy -Drevision=2.1.0Your CI pipeline controls the version. Your source code never changes for releases.
IDE Compatibility
IntelliJ handles ${revision} well in most cases. If you see resolution warnings:
Reimport the Maven project
Ensure you’re using a recent IntelliJ version (2021+)
Check that
relativePathis correctly set in child modules
Other IDEs may vary in support, but most modern IDEs have caught up with Maven’s capabilities.
Verifying Published POMs
After deploying, check that your published POMs have resolved versions:
# Download and inspect the published POM
mvn dependency:get -Dartifact=com.example:sdk-common:2.1.0:pom
cat ~/.m2/repository/com/example/sdk-common/2.1.0/sdk-common-2.1.0.pomYou should see <version>2.1.0</version>, not <version>${revision}</version>.
Trade-offs
This pattern isn’t free:
All modules version together: You can’t release
sdk-auth:2.2.0while keepingsdk-common:2.1.0. This pattern assumes a single coordinated release of all modules, which is typical for internal SDKs but may not fit independently versioned libraries.IDE quirks: Some IDEs occasionally struggle with property-based versions. The workarounds are minor but exist.
One more plugin: The flatten plugin is well-maintained and widely used, but it’s still a build-time dependency.
Working tree appears modified: The plugin generates a temporary flattened POM (written as
.flattened-pom.xml) used for deployment. If a build fails beforeclean, your working tree looks dirty. Add.flattened-pom.xmlto.gitignore.
For most multi-module projects—especially SDKs where consumers expect coordinated versions—these trade-offs are worth it.
Conclusion
One property. One plugin. Zero version-bump PRs.
The ${revision} pattern with flatten-maven-plugin removes an entire class of release mistakes. Your CI controls the version, your POMs stay clean, and consumers see properly resolved artifacts.
Next up: how to structure the dependency management layer with a Maven BOM.