Spring Boot Dependency Management: Maven & Gradle

Introduction
Every Spring Boot project starts with a build file — pom.xml or build.gradle. These files define which libraries your application depends on, how they're resolved, and how your project is built. Getting dependency management right means fewer version conflicts, faster builds, and a codebase that's easy to maintain.
In this comprehensive guide, we'll cover:
- How Maven and Gradle handle dependencies
- Spring Boot Starters and auto-configuration
- The Spring Boot BOM (Bill of Materials)
- Managing transitive dependencies and version conflicts
- Multi-module project setup
- Dependency security scanning
- Best practices for production-ready builds
Prerequisites: Familiarity with Java and basic Spring Boot concepts. If you're new to Spring Boot, start with Getting Started with Spring Boot.
Maven vs Gradle: Choosing Your Build Tool
Both Maven and Gradle are excellent choices. Here's a quick comparison:
| Feature | Maven | Gradle |
|---|---|---|
| Configuration | XML (pom.xml) | Groovy/Kotlin DSL (build.gradle) |
| Build Speed | Good (with -T parallel) | Faster (incremental + cache) |
| Learning Curve | Lower (convention-heavy) | Moderate (flexible DSL) |
| Ecosystem | Largest plugin ecosystem | Growing, Kotlin-first |
| Spring Boot Support | First-class | First-class |
| Dependency Locking | Via plugin | Built-in |
| Build Cache | Local only | Local + remote |
Recommendation: Use Maven if your team is already familiar with it. Choose Gradle for new projects where build speed matters — especially in CI/CD pipelines.
Understanding Spring Boot Starters
Spring Boot Starters are curated dependency bundles that pull in everything you need for a specific feature. Instead of manually adding 5-10 individual libraries, you add a single starter.
How Starters Work
Adding spring-boot-starter-web gives you an embedded Tomcat server, Jackson for JSON, Spring MVC, and all their transitive dependencies — with compatible versions guaranteed.
Common Starters Reference
| Starter | What It Includes |
|---|---|
spring-boot-starter-web | Spring MVC, Tomcat, Jackson |
spring-boot-starter-data-jpa | Hibernate, Spring Data JPA, HikariCP |
spring-boot-starter-security | Spring Security, authentication filters |
spring-boot-starter-validation | Hibernate Validator, Jakarta Validation |
spring-boot-starter-actuator | Health checks, metrics, monitoring endpoints |
spring-boot-starter-cache | Spring Cache abstraction |
spring-boot-starter-mail | JavaMail, Spring mail integration |
spring-boot-starter-test | JUnit 5, Mockito, AssertJ, MockMvc |
spring-boot-starter-webflux | Reactive web with Netty |
spring-boot-starter-amqp | RabbitMQ integration |
spring-boot-starter-data-redis | Redis client, Spring Data Redis |
What's NOT a Starter
Not every Spring Boot dependency is a starter. These are standalone libraries you add separately:
<!-- These are NOT starters — add individually -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>Maven Dependency Management
Project Setup with Spring Initializr
The easiest way to start is Spring Initializr. It generates a pom.xml with the Spring Boot parent POM and your selected starters.
The Spring Boot Parent POM
Every Maven-based Spring Boot project inherits from spring-boot-starter-parent:
<?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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Inherit Spring Boot defaults -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>my-app</name>
<description>My Spring Boot Application</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- No version needed! Inherited from parent -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Key points:
- No version numbers on Spring Boot starters — the parent POM manages them
spring-boot-starter-parentextendsspring-boot-dependenciesBOM- Over 400+ library versions are pre-defined and tested together
- The
spring-boot-maven-pluginpackages your app as an executable JAR
Understanding the Parent POM Chain
The parent POM provides:
- Dependency versions: All Spring ecosystem + common third-party libraries
- Plugin defaults: Compiler version, resource filtering, packaging
- Profile support: Production-ready defaults
- Property overrides: Easy version customization via
<properties>
Using the BOM Without the Parent POM
If your project already has a parent POM (e.g., a corporate parent), import the Spring Boot BOM instead:
<project>
<!-- Your corporate parent -->
<parent>
<groupId>com.company</groupId>
<artifactId>company-parent</artifactId>
<version>2.0.0</version>
</parent>
<dependencyManagement>
<dependencies>
<!-- Import Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.4.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- Still no version needed -->
</dependency>
</dependencies>
</project>Dependency Scopes in Maven
Maven scopes control when a dependency is available:
| Scope | Compile | Test | Runtime | Packaged |
|---|---|---|---|---|
compile (default) | Yes | Yes | Yes | Yes |
provided | Yes | Yes | No | No |
runtime | No | Yes | Yes | Yes |
test | No | Yes | No | No |
system | Yes | Yes | No | No |
<dependencies>
<!-- compile scope (default) — always available -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- runtime — needed at runtime only, not for compilation -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- provided — available at compile time, provided by container at runtime -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- test — only for testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>Overriding Managed Versions
Need a different version of a managed dependency? Override it via properties:
<properties>
<java.version>21</java.version>
<!-- Override managed versions -->
<jackson.version>2.16.1</jackson.version>
<snakeyaml.version>2.2</snakeyaml.version>
<postgresql.version>42.7.1</postgresql.version>
</properties>To find which property controls a dependency version, check the spring-boot-dependencies POM.
Viewing the Dependency Tree
The dependency tree is your best debugging tool:
# Full dependency tree
mvn dependency:tree
# Filter for a specific artifact
mvn dependency:tree -Dincludes=com.fasterxml.jackson
# Show conflicts and duplicates
mvn dependency:tree -Dverbose
# Output to file for large projects
mvn dependency:tree -DoutputFile=deps.txtExample output:
[INFO] com.example:my-app:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.4.3:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:3.4.3:compile
[INFO] | | +- org.springframework.boot:spring-boot:jar:3.4.3:compile
[INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:3.4.3:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:3.4.3:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:3.4.3:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.16.1:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:3.4.3:compile
[INFO] | +- org.springframework:spring-webmvc:jar:6.2.3:compileExcluding Transitive Dependencies
Sometimes a starter pulls in a library you don't want:
<!-- Switch from Tomcat to Jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency><!-- Exclude default logging (Logback) for Log4j2 -->
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>Maven Profiles for Environment-Specific Dependencies
<profiles>
<!-- Development profile -->
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
</profile>
<!-- Production profile -->
<profile>
<id>prod</id>
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</profile>
</profiles>Activate profiles:
# Development (default)
mvn spring-boot:run
# Production
mvn spring-boot:run -Pprod
# Multiple profiles
mvn spring-boot:run -Pprod,monitoringGradle Dependency Management
Project Setup
A Gradle-based Spring Boot project uses the Spring Boot Gradle plugin:
// build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.3'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.postgresql:postgresql'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}Gradle Kotlin DSL
Modern projects often use Kotlin DSL for type-safe build scripts:
// build.gradle.kts
plugins {
java
id("org.springframework.boot") version "3.4.3"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("org.postgresql:postgresql")
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<Test> {
useJUnitPlatform()
}Dependency Configurations in Gradle
Gradle uses configurations instead of Maven's scopes:
| Configuration | Maven Equivalent | When Available |
|---|---|---|
implementation | compile | Compile + runtime, hidden from consumers |
api | compile | Compile + runtime, exposed to consumers |
compileOnly | provided | Compile only |
runtimeOnly | runtime | Runtime only |
testImplementation | test | Test compile + runtime |
annotationProcessor | N/A | Annotation processing at compile |
implementation vs api: Use implementation by default. Use api only if the dependency is part of your module's public API. This improves build performance because changes to implementation dependencies don't trigger recompilation of downstream modules.
Overriding Versions in Gradle
// Method 1: Via ext properties (same as Maven properties)
ext['jackson.version'] = '2.16.1'
ext['snakeyaml.version'] = '2.2'
// Method 2: Dependency constraints
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// Force a specific version
constraints {
implementation('com.fasterxml.jackson.core:jackson-databind:2.16.1') {
because 'CVE fix requires newer version'
}
}
}
// Method 3: Resolution strategy
configurations.all {
resolutionStrategy {
force 'com.google.guava:guava:33.0.0-jre'
}
}Viewing Dependencies in Gradle
# Full dependency tree
./gradlew dependencies
# Specific configuration
./gradlew dependencies --configuration runtimeClasspath
# Search for a specific dependency
./gradlew dependencyInsight --dependency jackson-databind
# HTML report
./gradlew htmlDependencyReportExcluding Dependencies in Gradle
dependencies {
// Exclude from a specific dependency
implementation('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
implementation 'org.springframework.boot:spring-boot-starter-jetty'
// Global exclusion
configurations.all {
exclude group: 'commons-logging', module: 'commons-logging'
}
}Dependency Locking in Gradle
Lock dependency versions for reproducible builds:
// Enable dependency locking
dependencyLocking {
lockAllConfigurations()
}# Generate lock files
./gradlew dependencies --write-locks
# Update locks
./gradlew dependencies --update-locks org.springframework.boot:*This creates gradle.lockfile in your project root — commit it to version control.
Managing Transitive Dependencies
What Are Transitive Dependencies?
When you add spring-boot-starter-web, it doesn't just add itself — it pulls in dozens of transitive dependencies:
Version Conflict Resolution
When two dependencies require different versions of the same library:
Maven's strategy: Nearest definition wins (shortest path in dependency tree)
A → B → C:1.0
A → D → E → C:2.0
Maven picks C:1.0 (nearer to A)Gradle's strategy: Highest version wins
A → B → C:1.0
A → D → E → C:2.0
Gradle picks C:2.0 (higher version)Diagnosing Version Conflicts
Maven:
# Show verbose tree with conflict markers
mvn dependency:tree -Dverbose
# Analyze dependency issues
mvn dependency:analyze
# Shows: unused declared dependencies, used undeclared dependenciesGradle:
# Show dependency insight with conflict resolution
./gradlew dependencyInsight --dependency jackson-databind --configuration runtimeClasspathExample output showing a conflict resolution:
com.fasterxml.jackson.core:jackson-databind:2.16.1
Variant runtimeElements:
Selected by rule: Spring Boot BOM
Requested: 2.15.0 → 2.16.1
com.fasterxml.jackson.core:jackson-databind:2.15.0 -> 2.16.1
\--- com.some-library:some-lib:1.0.0
\--- runtimeClasspathThe Optional Flag
Mark dependencies as optional when they're not required for consumers of your library:
<!-- Maven -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>// Gradle
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'Optional dependencies are NOT included transitively. Consumers must explicitly add them if needed.
Multi-Module Projects
For larger applications, split your project into modules:
Maven Multi-Module Structure
my-app/
├── pom.xml (parent POM)
├── my-app-common/
│ └── pom.xml (shared DTOs, utilities)
├── my-app-domain/
│ └── pom.xml (entities, repositories)
├── my-app-service/
│ └── pom.xml (business logic)
└── my-app-web/
└── pom.xml (REST controllers, main app)Parent POM (pom.xml):
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
</parent>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>my-app-common</module>
<module>my-app-domain</module>
<module>my-app-service</module>
<module>my-app-web</module>
</modules>
<properties>
<java.version>21</java.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>
<!-- Shared dependency management for non-Spring libraries -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>Domain module (my-app-domain/pom.xml):
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>my-app-domain</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>my-app-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
</project>Web module (my-app-web/pom.xml):
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>my-app-web</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>my-app-service</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Gradle Multi-Module Structure
settings.gradle.kts:
rootProject.name = "my-app"
include("my-app-common")
include("my-app-domain")
include("my-app-service")
include("my-app-web")Root build.gradle.kts:
plugins {
java
id("org.springframework.boot") version "3.4.3" apply false
id("io.spring.dependency-management") version "1.1.7" apply false
}
subprojects {
apply(plugin = "java")
apply(plugin = "io.spring.dependency-management")
group = "com.example"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
repositories {
mavenCentral()
}
// Apply BOM to all subprojects
the<io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension>().apply {
imports {
mavenBom("org.springframework.boot:spring-boot-dependencies:3.4.3")
}
}
dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
}Web module build.gradle.kts:
plugins {
id("org.springframework.boot")
}
dependencies {
implementation(project(":my-app-service"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
}Dependency Security
Why It Matters
Third-party dependencies can introduce security vulnerabilities. A single vulnerable transitive dependency can expose your entire application. Regular scanning is essential.
OWASP Dependency-Check
Maven:
<build>
<plugins>
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.9</version>
<configuration>
<!-- Fail build on CVSS >= 7 -->
<failBuildOnCVSS>7</failBuildOnCVSS>
</configuration>
</plugin>
</plugins>
</build>mvn dependency-check:checkGradle:
plugins {
id 'org.owasp.dependencycheck' version '9.0.9'
}
dependencyCheck {
failBuildOnCVSS = 7.0f
analyzers {
assemblyEnabled = false
}
}./gradlew dependencyCheckAnalyzeAutomated Dependency Updates
Dependabot (GitHub-native):
Create .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
groups:
spring-boot:
patterns:
- "org.springframework.boot*"
- "org.springframework*"
jackson:
patterns:
- "com.fasterxml.jackson*"Renovate (alternative to Dependabot with more features):
Create renovate.json:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":automergeMinor",
"group:springBoot"
],
"packageRules": [
{
"matchPackagePatterns": ["org.springframework"],
"groupName": "Spring Framework",
"automerge": false
}
]
}Spring Boot's Built-in Security
Spring Boot already mitigates many dependency risks:
- BOM-managed versions: All dependencies tested together for compatibility
- CVE patching: Security patches land quickly in Spring Boot releases
- Dependency convergence: The BOM ensures consistent versions across the entire dependency tree
Common Patterns and Recipes
Recipe 1: Adding a Custom BOM
When using additional library ecosystems (e.g., AWS SDK, Google Cloud):
<!-- Maven -->
<dependencyManagement>
<dependencies>
<!-- AWS SDK BOM -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.24.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Google Cloud BOM -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.31.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- No version needed — managed by AWS BOM -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
</dependency>
</dependencies>// Gradle
dependencies {
implementation(platform("software.amazon.awssdk:bom:2.24.0"))
implementation("software.amazon.awssdk:s3")
implementation("software.amazon.awssdk:dynamodb")
}Recipe 2: Replacing Embedded Server
<!-- Switch from Tomcat to Undertow -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
</dependencies>Recipe 3: Test-Only Dependencies
<!-- Maven -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>// Gradle
dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.testcontainers:postgresql")
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("io.rest-assured:rest-assured")
}Recipe 4: Annotation Processors
<!-- Maven — Lombok + MapStruct together -->
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</path>
<!-- Lombok-MapStruct binding -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>// Gradle — much simpler
dependencies {
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
implementation("org.mapstruct:mapstruct:1.5.5.Final")
annotationProcessor("org.mapstruct:mapstruct-processor:1.5.5.Final")
annotationProcessor("org.projectlombok:lombok-mapstruct-binding:0.2.0")
}Best Practices
1. Let the BOM Manage Versions
<!-- ✅ Good — version managed by Spring Boot BOM -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- ❌ Bad — hardcoded version that may conflict -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>Only override versions when you have a specific reason (security fix, bug fix, new feature).
2. Use the Correct Scope
<!-- ✅ Good — PostgreSQL driver only needed at runtime -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- ❌ Bad — default compile scope exposes driver API unnecessarily -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>3. Keep Dependencies Minimal
<!-- ✅ Good — only what you need -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ❌ Bad — pulling in everything "just in case" -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>4. Audit Regularly
# Maven: find unused and undeclared dependencies
mvn dependency:analyze
# Output:
# [WARNING] Unused declared dependencies:
# org.springframework.boot:spring-boot-starter-mail:jar:3.4.3:compile
# [WARNING] Used undeclared dependencies:
# org.springframework:spring-context:jar:6.2.3:compile- Unused declared: You declared it but never use it — remove it
- Used undeclared: You use it but only get it transitively — declare it explicitly
5. Pin Third-Party Library Versions
For libraries NOT managed by the Spring Boot BOM:
<!-- ✅ Good — explicit version in properties -->
<properties>
<springdoc.version>2.3.0</springdoc.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
</dependencies>6. Upgrade Spring Boot Systematically
# 1. Check current version
mvn help:evaluate -Dexpression=project.parent.version -q -DforceStdout
# 2. Update parent version in pom.xml (e.g., 3.3.x → 3.4.x)
# 3. Check for deprecated APIs
mvn compile -Xlint:deprecation
# 4. Run full test suite
mvn test
# 5. Check dependency tree for conflicts
mvn dependency:tree -Dverbose
# 6. Review release notes for breaking changes7. Use Maven Wrapper or Gradle Wrapper
Always commit the wrapper to ensure consistent build tool versions across the team:
# Maven — generate wrapper
mvn wrapper:wrapper -Dmaven=3.9.6
# Use wrapper instead of global Maven
./mvnw clean install
./mvnw spring-boot:run# Gradle — generate wrapper
gradle wrapper --gradle-version 8.5
# Use wrapper instead of global Gradle
./gradlew clean build
./gradlew bootRunTroubleshooting Common Issues
"ClassNotFoundException" or "NoClassDefFoundError"
Cause: Missing dependency or wrong scope.
# Check if the class exists in your dependency tree
mvn dependency:tree -Dincludes=*:*:*:*:* # full tree
# Search for the package/class in your dependenciesFix: Add the dependency explicitly or change its scope from test/provided to compile.
"Multiple Bindings" in SLF4J
Cause: Multiple logging implementations on the classpath.
mvn dependency:tree -Dincludes=org.slf4j
mvn dependency:tree -Dincludes=ch.qos.logback
mvn dependency:tree -Dincludes=org.apache.logging.log4jFix: Exclude the unwanted logging framework:
<dependency>
<groupId>some-library</groupId>
<artifactId>some-artifact</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>"Bean Definition Override" Errors
Cause: Two starters providing the same auto-configuration bean.
Fix: Check for conflicting starters and exclude one, or set:
# application.properties
spring.main.allow-bean-definition-overriding=trueDependency Convergence Failures
Cause: Different versions of the same dependency in the tree.
Maven — enforcer plugin:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>enforce-dependency-convergence</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<dependencyConvergence/>
<requireJavaVersion>
<version>[21,)</version>
</requireJavaVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>Quick Reference: Complete pom.xml Template
Here's a production-ready pom.xml template with common Spring Boot dependencies:
<?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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>21</java.version>
<springdoc.version>2.3.0</springdoc.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Data -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Monitoring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- API Documentation -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!-- Development -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>Summary
Spring Boot's dependency management simplifies one of the hardest parts of Java development — keeping your libraries compatible and up to date.
Key takeaways:
✅ Use Spring Boot Starters to get pre-configured dependency bundles
✅ Let the BOM manage versions — don't hardcode them
✅ Use mvn dependency:tree or ./gradlew dependencies to debug conflicts
✅ Exclude transitive dependencies when switching implementations (e.g., Tomcat → Jetty)
✅ Set correct scopes (runtime, test, provided) to keep your classpath clean
✅ Scan for vulnerabilities with OWASP Dependency-Check
✅ Automate updates with Dependabot or Renovate
✅ Always use the Maven/Gradle wrapper for reproducible builds
Related posts:
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.