Maven Multi-Module Project Dependency Resolution with Spring Boot 3.4+
When using Spring Boot 3.4+ in Maven multi-module projects, you might encounter dependency resolution issues:
<!-- Error: Could not resolve dependencies for project -->
[ERROR] Failed to execute goal on project my-service:
Could not resolve dependencies for project com.example:my-service:jar:1.0.0:
The following artifacts could not be resolved:
org.springframework.boot:spring-boot-starter-web:jar:3.4.0,
org.springframework.boot:spring-boot-starter-data-jpa:jar:3.4.0
Or runtime conflicts:
// ClassNotFoundException or NoClassDefFoundError
java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.EnableAutoConfiguration
java.lang.NoClassDefFoundError: org/springframework/boot/context/annotation/ConfigurationProperties
What causes this and how can it be fixed?
Solution
The issue is caused by Spring Boot 3.4+ introducing changes that affect Maven dependency resolution in multi-module projects, including stricter dependency validation, parent POM inheritance issues, and dependency scope conflicts.
Use proper parent POM structure:
<!-- parent/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>common</module>
<module>api</module>
<module>service</module>
<module>web</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath/>
</parent>
<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Child module configuration:
<!-- service/pom.xml -->
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<relativePath>../parent</relativePath>
</parent>
<artifactId>service</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Always use dependency management in parent POM and ensure proper module references.
Alternative #1
If you're dealing with dependency conflicts or version mismatches, use explicit dependency management and exclusions. This approach gives you fine-grained control over which versions are used and helps resolve conflicts between different Spring modules.
Add to your parent POM:
<dependencyManagement>
<dependencies>
<!-- Force specific versions -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.2.0</version>
</dependency>
<!-- Exclude conflicting dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>
Useful troubleshooting commands include cleaning and rebuilding with mvn clean install -U
, analyzing dependency trees with mvn dependency:tree
, and checking for conflicts with mvn dependency:analyze
. You can also force dependency updates with mvn dependency:resolve -U
.
This approach is particularly useful when you have specific version requirements or need to resolve conflicts between different Spring modules. You can also use BOM (Bill of Materials) imports to ensure all Spring dependencies use compatible versions:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>6.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
This ensures all Spring dependencies use compatible versions automatically.
Alternative #2
For complex enterprise applications with many modules, consider using Maven profiles and conditional dependencies to handle different environments. This approach provides better dependency isolation and makes testing and deployment easier.
Create environment-specific profiles:
<profiles>
<profile>
<id>development</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</profile>
<profile>
<id>production</id>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>
Use dependency scopes to avoid conflicts:
<!-- Use provided scope for internal dependencies -->
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<scope>provided</scope>
</dependency>
<!-- Use runtime scope for optional dependencies -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
This approach offers several advantages including environment-specific configurations, better dependency isolation, and more flexible module relationships. It's especially useful when you have multiple deployment environments or need conditional dependencies based on build context.
The migration strategy involves starting with basic dependency management, then adding profiles for different environments. Use appropriate dependency scopes, test build order and dependencies, and monitor for conflicts. This approach works well for enterprise applications that need to handle multiple deployment scenarios or have complex dependency requirements.