DevOps & CI/CD

My MJFS Stack Nightmare: 3 Critical Fixes for Maven 4 in 2025

Facing an MJFS stack nightmare with Maven 4? Discover 3 critical fixes for dependency conflicts, plugin compatibility, and broken builds in 2025. Get your CI/CD pipeline back on track.

A

Alexei Volkov

Senior DevOps Engineer specializing in JVM build systems and CI/CD pipeline optimization.

7 min read4 views

Introduction: The 4 AM Jenkins Alert

It was one of those alerts that every DevOps engineer dreads. A red build, then another, then a cascade of failures across our entire MJFS (Maven, Jenkins, Flyway, Spring Boot) stack. The culprit? Our well-intentioned, C-level-approved initiative to upgrade our build toolchain to Maven 4. We were promised faster, more reliable builds. Instead, we got a full-blown production pipeline nightmare. The year is 2025, and Maven 4 is no longer a niche upgrade; it's the standard. But making it play nice with a mature, complex stack requires more than just changing a version number.

After days of debugging, frantic `pom.xml` archaeology, and enough coffee to power a small data center, we isolated the core issues. This wasn't about a single bug; it was a fundamental shift in how Maven operates. If you're staring down a similar migration or wrestling with mysterious build failures, this post is your lifeline. We're going to walk through the three critical fixes that tamed Maven 4 and stabilized our entire MJFS pipeline.

The Maven 4 Paradox: Faster Builds, Faster Failures

Maven 4 is genuinely a leap forward. Its parallel builds are significantly faster, the dependency resolver is smarter, and the console output is cleaner. On a simple "Hello, World" Spring Boot project, the benefits are immediate and impressive. However, in a real-world MJFS environment, these very improvements become double-edged swords.

  • The Stricter Dependency Resolver: Maven 4's dependency mediation is less forgiving. It no longer silently accepts certain types of version conflicts that Maven 3 let slide. This is good for correctness but bad for legacy projects with years of accumulated dependency debt.
  • Plugin API Changes: Under the hood, Maven's core has evolved. Plugins that haven't been explicitly updated for Maven 4 can behave erratically or fail silently, especially when interacting with the build lifecycle in a Jenkins pipeline.
  • Profile and Property Refinements: The way profiles are activated and properties are inherited has been tightened. Complex `pom.xml` files with multiple, overlapping profiles can suddenly produce different build artifacts, leading to chaos in deployment environments managed by Jenkins and Flyway.

Our nightmare wasn't because Maven 4 is broken. It was because our stack, like many, had developed a reliance on Maven 3's quirks and leniency. The upgrade simply exposed all our hidden technical debt at once.

Fix #1: Taming the New Dependency Resolver

The Symptom: Transitive Dependency Hell Returns

The first sign of trouble was the classic `NoSuchMethodError` or `ClassNotFoundException` at runtime, even though the project compiled perfectly. Our Jenkins builds were green, but the deployed Spring Boot application would crash. This happens because Maven 4's resolver might pick a different version of a transitive dependency (a dependency of your dependency) than Maven 3 did. For example, `spring-boot-starter-web` might bring in a version of Jackson, while another library, say `some-legacy-connector`, brings in an older, incompatible version. Maven 3 might have picked the one that worked by chance; Maven 4's stricter algorithm might pick the other.

The Cure: Radical Explicitness with `dependencyManagement`

You can no longer leave transitive dependency versions to chance. The solution is to explicitly declare the versions of all critical, shared dependencies in your parent POM's `` section. This acts as a bill of materials (BOM) for your entire project, forcing all modules to align on a single version.

Step 1: Identify the Conflicts
Run the verbose dependency tree command to see exactly which versions are being chosen:

mvn dependency:tree -Dverbose

Look for lines where different versions of the same library are being considered, and see which one Maven ultimately picks.

Step 2: Enforce Versions with `dependencyManagement`
In your top-level `pom.xml`, add or augment the `` block to explicitly set the versions for any libraries causing conflict. This is far superior to using `` tags on every single dependency declaration.

<dependencyManagement>
    <dependencies>
        <!-- This is our project's BOM -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.4.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <!-- Explicitly override a problematic transitive dependency -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.0</version> <!-- Force this version everywhere -->
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>33.0.0-jre</version>
        </dependency>
    </dependencies>
</dependencyManagement>

By doing this, you're creating a single source of truth for dependency versions, removing the ambiguity that Maven 4's resolver struggles with.

Fix #2: Overhauling Plugin Configurations for Compatibility

The Symptom: Silent Plugin Failures in CI

This was the most insidious problem. Our Jenkins jobs would complete successfully, but the resulting artifact would be subtly wrong. For example, the `maven-shade-plugin` might not correctly relocate classes, or the `flyway-maven-plugin` might not execute migrations against the profile-specific database. This is often because older plugin versions rely on internal Maven APIs that have changed in version 4. They don't throw an error; they just don't work as expected.

The Cure: Audit, Update, and Modernize

You must treat your build plugins as first-class dependencies. Audit every single plugin defined in your POMs and their parents.

  1. Check for Maven 4 Compatibility: Visit the homepage for each plugin and check its documentation for explicit Maven 4 support.
  2. Update Versions: Update all plugins to their latest stable versions. Don't assume an old version will work.
  3. Modernize Configuration: Newer plugin versions often have simplified and more robust configuration options.

This is where a comparison becomes invaluable:

Table 1: Plugin Configuration Modernization for Maven 4
Plugin & GoalOld (Maven 3-era) ConfigurationModern (Maven 4-ready) Configuration
maven-compiler-pluginRelied on default properties, sometimes omitting version. Configuration could be sparse.Explicitly set `source` and `target` or, better, the `release` property. Lock down the plugin version.
maven-failsafe-pluginOften configured with complex `systemPropertyVariables` inside the plugin's XML block.Uses `@argLine` property for JaCoCo and other agents, which is cleaner and less prone to conflicts.
flyway-maven-pluginJDBC URL, user, and password hardcoded in profiles. A major security and flexibility issue.Configuration pulls properties from `settings.xml` servers or environment variables, making it portable for Jenkins.

Fix #3: Refactoring Build Profiles for Predictability

The Symptom: The Dreaded "It Works On My Machine" Syndrome

A developer would run `mvn clean install -Pdev` locally and everything would pass. The Jenkins job, running the exact same command, would fail or produce a bad artifact. The cause? Maven 4's refined profile activation logic. A profile activated by the absence of a file on a developer's machine might not be activated on a clean Jenkins agent, or vice-versa. Overlapping profiles that define the same property could also be resolved in a different order, leading to unpredictable behavior.

The Cure: Simplify Profiles, Externalize Properties

The goal is to make your build as deterministic as possible, removing reliance on the environment. Your build should not behave differently because of an OS or the presence of a file.

Before: Complex, Environment-Sensing Profiles

<profiles>
    <profile>
        <id>dev</id>
        <activation>
            <property>
                <name>env</name>
                <value>dev</value>
            </property>
        </activation>
        <properties>
            <database.url>jdbc:h2:mem:devdb</database.url>
        </properties>
    </profile>
    <profile>
        <id>prod-build</id>
        <activation>
            <property>
                <name>env</name>
                <value>!dev</value>
            </property>
        </activation>
        <properties>
            <database.url>jdbc:postgresql://prod-host/appdb</database.url>
        </properties>
    </profile>
</profiles>

This is fragile. The `!dev` activation is a recipe for disaster.

After: Simple, Explicit Profiles with Externalized Config
The profiles themselves should only contain structural differences, not secrets or environment-specific values. The values should be passed in by the execution environment (i.e., Jenkins).

<profiles>
    <profile>
        <id>integration-tests</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-failsafe-plugin</artifactId>
                    <executions>...</executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

<!-- In your pom.xml, reference properties -->
<properties>
    <database.url>${env.DB_URL}</database.url>
    <database.user>${env.DB_USER}</database.user>
</properties>

Then, in your Jenkinsfile or build script, you explicitly activate the profiles and pass the properties:

mvn clean verify -Pintegration-tests -Denv.DB_URL=jdbc:... -Denv.DB_USER=...

This approach makes the build's behavior explicit and repeatable, eliminating the guesswork and restoring sanity to your CI/CD pipeline.

Conclusion: From Nightmare to a Dream Build

The transition to Maven 4 within our MJFS stack was painful, but it forced us to pay off years of technical debt. By rigorously managing our dependencies, auditing our plugins, and simplifying our build profiles, we not only solved the immediate crisis but also created a more robust, faster, and more secure build system for the future. The nightmare was a blessing in disguise.

Don't fear Maven 4. Embrace the changes, but do so with a clear strategy. The performance and correctness gains are real and worth the effort. Your 4 AM on-call self will thank you.