Versioning a Multi-Module Maven SDK Without Breaking CI

This is Part 1 of a series: Maven Patterns from Building an Internal SDK.

  • Part 1: Versioning a Multi-Module Maven SDK Without Breaking CI (this post)

  • Part 2: Designing an Internal Maven BOM for SDKs

  • Part 3: Custom Spring Boot Starters: What Worked and What Didn’t

  • Part 4: Using Maven Enforcer to Keep SDK Consumers Sane

  • Part 5: Code Quality Gates in a Maven SDK Build

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.0 depending on sdk-common:2.0.0 and production is broken.

  • Using ${project.version} with dependencyManagement: 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.0

Your 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 relativePath is 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.pom

You 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.0 while keeping sdk-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 before clean, your working tree looks dirty. Add .flattened-pom.xml to .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.