Back to blog

Spring Boot Dependency Management: Maven & Gradle

javaspring-bootmavengradlebackend
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:

FeatureMavenGradle
ConfigurationXML (pom.xml)Groovy/Kotlin DSL (build.gradle)
Build SpeedGood (with -T parallel)Faster (incremental + cache)
Learning CurveLower (convention-heavy)Moderate (flexible DSL)
EcosystemLargest plugin ecosystemGrowing, Kotlin-first
Spring Boot SupportFirst-classFirst-class
Dependency LockingVia pluginBuilt-in
Build CacheLocal onlyLocal + 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

StarterWhat It Includes
spring-boot-starter-webSpring MVC, Tomcat, Jackson
spring-boot-starter-data-jpaHibernate, Spring Data JPA, HikariCP
spring-boot-starter-securitySpring Security, authentication filters
spring-boot-starter-validationHibernate Validator, Jakarta Validation
spring-boot-starter-actuatorHealth checks, metrics, monitoring endpoints
spring-boot-starter-cacheSpring Cache abstraction
spring-boot-starter-mailJavaMail, Spring mail integration
spring-boot-starter-testJUnit 5, Mockito, AssertJ, MockMvc
spring-boot-starter-webfluxReactive web with Netty
spring-boot-starter-amqpRabbitMQ integration
spring-boot-starter-data-redisRedis 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-parent extends spring-boot-dependencies BOM
  • Over 400+ library versions are pre-defined and tested together
  • The spring-boot-maven-plugin packages your app as an executable JAR

Understanding the Parent POM Chain

The parent POM provides:

  1. Dependency versions: All Spring ecosystem + common third-party libraries
  2. Plugin defaults: Compiler version, resource filtering, packaging
  3. Profile support: Production-ready defaults
  4. 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:

ScopeCompileTestRuntimePackaged
compile (default)YesYesYesYes
providedYesYesNoNo
runtimeNoYesYesYes
testNoYesNoNo
systemYesYesNoNo
<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.txt

Example 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:compile

Excluding 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,monitoring

Gradle 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:

ConfigurationMaven EquivalentWhen Available
implementationcompileCompile + runtime, hidden from consumers
apicompileCompile + runtime, exposed to consumers
compileOnlyprovidedCompile only
runtimeOnlyruntimeRuntime only
testImplementationtestTest compile + runtime
annotationProcessorN/AAnnotation 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 htmlDependencyReport

Excluding 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 dependencies

Gradle:

# Show dependency insight with conflict resolution
./gradlew dependencyInsight --dependency jackson-databind --configuration runtimeClasspath

Example 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
     \--- runtimeClasspath

The 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:check

Gradle:

plugins {
    id 'org.owasp.dependencycheck' version '9.0.9'
}
 
dependencyCheck {
    failBuildOnCVSS = 7.0f
    analyzers {
        assemblyEnabled = false
    }
}
./gradlew dependencyCheckAnalyze

Automated 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 changes

7. 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 bootRun

Troubleshooting 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 dependencies

Fix: 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.log4j

Fix: 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=true

Dependency 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.