Why Version Management Matters
Think of versions like recipe ingredients:
- Without version management: "Add some flour" - unclear, inconsistent results
- With version management: "Add 2 cups of flour" - precise, reproducible
Version management ensures:
- Reproducible builds: Same code + same versions = same result
- Security: Track and update vulnerable dependencies
- Compatibility: Avoid conflicts between libraries
- Maintainability: Centralized version updates
Semantic Versioning (SemVer)
// Version format: MAJOR.MINOR.PATCH
// 1 . 2 . 3
// Examples:
// 1.0.0 - First stable release
// 1.0.1 - Bug fix (PATCH)
// 1.1.0 - New feature, backward compatible (MINOR)
// 2.0.0 - Breaking changes (MAJOR)
// When to increment:
// MAJOR - Incompatible API changes (breaking)
// Example: Removed method, changed signature
// MINOR - Added functionality (backward compatible)
// Example: New method, new feature
// PATCH - Bug fixes (backward compatible)
// Example: Fixed null pointer, performance fix
| Version Change | Meaning | Safe to Upgrade? |
|---|---|---|
| 1.2.3 → 1.2.4 | Bug fix | Yes, always |
| 1.2.3 → 1.3.0 | New features | Yes, usually |
| 1.2.3 → 2.0.0 | Breaking changes | Review required |
SNAPSHOT vs Release Versions
<!-- SNAPSHOT - Development version -->
<version>1.0.0-SNAPSHOT</version>
<!-- Release - Stable, immutable version -->
<version>1.0.0</version>
| Aspect | SNAPSHOT | Release |
|---|---|---|
| Purpose | Development, testing | Production use |
| Mutability | Can change anytime | Immutable (never changes) |
| Maven behavior | Checks for updates daily | Downloads once, cached forever |
| Reproducibility | Not guaranteed | Guaranteed |
| In production? | Never! | Always |
Warning
Never deploy SNAPSHOT versions to production! They can change without notice, making builds unreproducible and potentially introducing bugs.
Other Version Suffixes
<!-- Common version qualifiers -->
1.0.0-SNAPSHOT <!-- Development -->
1.0.0-alpha <!-- Early testing, unstable -->
1.0.0-beta <!-- Feature complete, testing -->
1.0.0-RC1 <!-- Release Candidate 1 -->
1.0.0-M1 <!-- Milestone 1 -->
1.0.0 <!-- Final release -->
1.0.0-GA <!-- General Availability (same as final) -->
<!-- Version ordering (lowest to highest): -->
<!-- alpha < beta < milestone < rc < snapshot < final -->
Centralizing Versions with Properties
<project>
<properties>
<!-- Define versions in one place -->
<spring.version>6.1.0</spring.version>
<jackson.version>2.16.0</jackson.version>
<junit.version>5.10.0</junit.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<!-- Use property references -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version> <!-- Same version -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
</project>
Benefits
- Update version in one place, applies everywhere
- Ensures related libraries use matching versions
- Easy to review all versions at a glance
Bill of Materials (BOM)
A BOM is a special POM that defines a set of compatible dependency versions.
<!-- Import a BOM to manage versions automatically -->
<dependencyManagement>
<dependencies>
<!-- Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Jackson BOM -->
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.16.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Now you can omit versions! -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- Version managed by BOM -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<!-- Version managed by Jackson BOM -->
</dependency>
</dependencies>
Popular BOMs
| BOM | What It Manages |
|---|---|
spring-boot-dependencies |
All Spring Boot compatible libraries |
spring-framework-bom |
Spring Framework modules |
jackson-bom |
Jackson JSON library modules |
junit-bom |
JUnit 5 modules |
aws-sdk-java-pom |
AWS SDK modules |
Version Ranges (Use Carefully!)
<!-- Maven version range syntax -->
<!-- Exact version -->
<version>1.0</version> <!-- 1.0 (soft requirement) -->
<!-- Ranges -->
<version>[1.0]</version> <!-- Exactly 1.0 (hard requirement) -->
<version>[1.0,2.0)</version> <!-- 1.0 <= x < 2.0 -->
<version>[1.0,2.0]</version> <!-- 1.0 <= x <= 2.0 -->
<version>(1.0,2.0)</version> <!-- 1.0 < x < 2.0 -->
<version>[1.0,)</version> <!-- x >= 1.0 -->
<version>(,2.0]</version> <!-- x <= 2.0 -->
<!-- Example: Accept any 1.x version -->
<dependency>
<groupId>com.example</groupId>
<artifactId>my-lib</artifactId>
<version>[1.0,2.0)</version>
</dependency>
Avoid Version Ranges
Version ranges make builds non-reproducible. A build today might use 1.5, tomorrow 1.6. This can introduce unexpected bugs. Use exact versions instead.
Resolving Version Conflicts
# When two dependencies need different versions of the same library
# Check your dependency tree
mvn dependency:tree
# Example output showing conflict:
# [INFO] com.example:my-app:jar:1.0.0
# [INFO] +- org.springframework:spring-web:jar:6.1.0:compile
# [INFO] | \- org.springframework:spring-core:jar:6.1.0:compile
# [INFO] \- some-library:some-lib:jar:1.0:compile
# [INFO] \- org.springframework:spring-core:jar:5.3.0:compile (conflict!)
Solution 1: Exclude Transitive Dependency
<dependency>
<groupId>some-library</groupId>
<artifactId>some-lib</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency>
Solution 2: Force Version in dependencyManagement
<dependencyManagement>
<dependencies>
<!-- This version wins for ALL transitive dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
Maven's Conflict Resolution Rules
- Nearest definition wins: Closer to your project in the dependency tree
- First declaration wins: If same depth, first in POM order
- dependencyManagement overrides all: Versions declared here always win
Checking for Updates
# Check for newer versions of dependencies
mvn versions:display-dependency-updates
# Output:
# [INFO] The following dependencies have newer versions:
# [INFO] org.springframework:spring-core .......... 6.0.0 -> 6.1.0
# [INFO] com.fasterxml.jackson.core:jackson-core . 2.15.0 -> 2.16.0
# Check for plugin updates
mvn versions:display-plugin-updates
# Update versions in pom.xml (interactive)
mvn versions:use-latest-versions
# Update to latest release versions
mvn versions:use-latest-releases
# Update properties that define versions
mvn versions:update-properties
Version Management in Gradle
// build.gradle.kts (Kotlin DSL)
// Define versions in one place
val springVersion = "6.1.0"
val jacksonVersion = "2.16.0"
dependencies {
implementation("org.springframework:spring-core:$springVersion")
implementation("org.springframework:spring-web:$springVersion")
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
}
// Or use a BOM (platform)
dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.2.0"))
implementation("org.springframework.boot:spring-boot-starter-web") // No version!
}
// Version catalogs (recommended for larger projects)
// gradle/libs.versions.toml
// [versions]
// spring = "6.1.0"
// jackson = "2.16.0"
// [libraries]
// spring-core = { module = "org.springframework:spring-core", version.ref = "spring" }
Best Practices
DO:
- Use exact versions - Reproducible builds
- Centralize versions - Properties or BOMs
- Use BOMs - For compatible library sets
- Review updates regularly - Security patches
- Test after upgrades - Even minor versions
- Document major version changes - Migration notes
- Use dependencyManagement - Control transitive versions
DON'T:
- Don't use SNAPSHOT in production - Unreliable
- Don't use version ranges - Non-reproducible
- Don't mix major versions - spring-core 5 + spring-web 6
- Don't ignore security updates - CVE vulnerabilities
- Don't upgrade everything at once - Incremental upgrades
Summary
- Semantic Versioning: MAJOR.MINOR.PATCH for clear meaning
- SNAPSHOT: Development only, never production
- Properties: Centralize versions in one place
- BOM: Pre-tested compatible version sets
- Conflicts: Use exclusions or dependencyManagement
- Updates: Regular reviews with
versions:display-dependency-updates - Reproducibility: Exact versions ensure consistent builds