Gradle Version Catalog in Java: Modern Dependency Management

Gradle Version Catalogs provide a centralized way to manage dependencies and plugins across multiple Gradle projects. This guide covers implementation, best practices, and migration strategies.


Core Concepts

What is a Version Catalog?

  • Centralized version declarations for dependencies and plugins
  • Type-safe dependency references in build files
  • Shared across multiple modules and projects
  • Improves maintainability and consistency

Key Benefits:

  • Single Source of Truth: One place for version management
  • Type Safety: IDE support and refactoring capabilities
  • Consistency: Ensures all modules use same versions
  • Easy Updates: Update versions in one location

Implementation Guide

1. Version Catalog File Structure
# gradle/libs.versions.toml - Main version catalog file

[versions]

# Core dependencies spring-boot = "3.1.0" spring-cloud = "2022.0.3" jackson = "2.15.2" # Testing junit = "5.9.3" mockito = "5.3.1" testcontainers = "1.18.3" # Database h2 = "2.2.220" postgresql = "42.6.0" hibernate = "6.2.7.Final" # Utilities lombok = "1.18.28" mapstruct = "1.5.5.Final" guava = "32.1.1-jre" # Logging slf4j = "2.0.7" logback = "1.4.11" # Build plugins spotless = "6.19.0" jacoco = "0.8.10" git-properties = "2.4.1"

[libraries]

# Spring Boot spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" } spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa", version.ref = "spring-boot" } spring-boot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation", version.ref = "spring-boot" } spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "spring-boot" } spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring-boot" } # Spring Cloud spring-cloud-starter-config = { module = "org.springframework.cloud:spring-cloud-starter-config", version.ref = "spring-cloud" } spring-cloud-starter-bootstrap = { module = "org.springframework.cloud:spring-cloud-starter-bootstrap", version.ref = "spring-cloud" } # Database h2 = { module = "com.h2database:h2", version.ref = "h2" } postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } hibernate-core = { module = "org.hibernate.orm:hibernate-core", version.ref = "hibernate" } # Jackson jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" } # Testing junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" } testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" } # Utilities lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" } mapstruct = { module = "org.mapstruct:mapstruct", version.ref = "mapstruct" } mapstruct-processor = { module = "org.mapstruct:mapstruct-processor", version.ref = "mapstruct" } guava = { module = "com.google.guava:guava", version.ref = "guava" } # Logging slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } # Monitoring micrometer-core = { module = "io.micrometer:micrometer-core", version.ref = "micrometer" } micrometer-registry-prometheus = { module = "io.micrometer:micrometer-registry-prometheus", version.ref = "micrometer" }

[bundles]

# Predefined dependency groups spring-boot-web = ["spring-boot-starter-web", "spring-boot-starter-validation", "spring-boot-starter-actuator"] spring-data = ["spring-boot-starter-data-jpa", "hibernate-core"] testing = ["junit-jupiter", "mockito-core", "mockito-junit-jupiter", "spring-boot-starter-test"] database = ["postgresql", "h2"] logging = ["slf4j-api", "logback-classic"] monitoring = ["micrometer-core", "micrometer-registry-prometheus"]

[plugins]

spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.0" } jacoco = { id = "org.jacoco", version.ref = "jacoco" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } git-properties = { id = "com.gorylenko.gradle-git-properties", version.ref = "git-properties" }

2. Multi-Module Project Structure
project-root/
├── gradle/
│   └── libs.versions.toml
├── build.gradle.kts
├── settings.gradle.kts
├── api/
│   └── build.gradle.kts
├── core/
│   └── build.gradle.kts
├── service/
│   └── build.gradle.kts
└── infrastructure/
└── build.gradle.kts
3. Root build.gradle.kts
// build.gradle.kts - Root project
plugins {
// Plugins applied to root project only
alias(libs.plugins.spotless)
alias(libs.plugins.git.properties)
}
allprojects {
group = "com.example"
version = "1.0.0-SNAPSHOT"
repositories {
mavenCentral()
mavenLocal()
}
}
subprojects {
apply(plugin = "java")
apply(plugin = "jacoco")
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
tasks.withType<Test> {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
}
}
// Configure JaCoCo for test coverage
jacoco {
toolVersion = libs.versions.jacoco.get()
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true)
html.required.set(true)
csv.required.set(false)
}
}
}
// Spotless configuration for code formatting
spotless {
java {
target("**/*.java")
googleJavaFormat(libs.versions.spotless.get())
removeUnusedImports()
trimTrailingWhitespace()
endWithNewline()
}
kotlin {
target("**/*.kt")
ktlint(libs.versions.spotless.get())
trimTrailingWhitespace()
endWithNewline()
}
}
// Git properties for Spring Boot Actuator
gitProperties {
keys = listOf("git.branch", "git.commit.id", "git.commit.time")
}
// Task to display dependency updates
tasks.register("dependencyUpdates") {
doLast {
println("=== Dependency Versions ===")
libs.versions.forEach { (key, version) ->
println("$key: ${version.get()}")
}
}
}
4. Settings.gradle.kts
// settings.gradle.kts
rootProject.name = "my-application"
// Enable version catalog
enableFeaturePreview("VERSION_CATALOGS")
// Include subprojects
include(":api")
include(":core") 
include(":service")
include(":infrastructure")
include(":shared:utils")
include(":shared:models")
// Dependency resolution rules
dependencyResolutionManagement {
// Use version catalog
versionCatalogs {
create("libs") {
from(files("gradle/libs.versions.toml"))
}
}
repositories {
mavenCentral()
gradlePluginPortal()
}
}
5. Module build.gradle.kts Examples

API Module:

// api/build.gradle.kts
plugins {
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependency.management)
}
dependencies {
// Spring Boot bundles
implementation(platform(libs.spring.boot.get().toString()))
implementation(libs.bundles.spring.boot.web)
// Project modules
implementation(project(":core"))
implementation(project(":shared:utils"))
// Database
implementation(libs.bundles.database)
// Monitoring
implementation(libs.bundles.monitoring)
// Testing
testImplementation(libs.bundles.testing)
testImplementation(libs.testcontainers.postgresql)
}
springBoot {
buildInfo()
}

Core Module:

// core/build.gradle.kts
plugins {
id("java-library")
}
dependencies {
// Spring Data
api(libs.bundles.spring.data)
// Utilities
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
implementation(libs.mapstruct)
annotationProcessor(libs.mapstruct.processor)
implementation(libs.guava)
// Jackson for JSON processing
implementation(libs.jackson.databind)
implementation(libs.jackson.datatype.jsr310)
// Testing
testImplementation(libs.bundles.testing)
testImplementation(libs.testcontainers.postgresql)
testRuntimeOnly(libs.h2)
}
tasks.withType<JavaCompile> {
options.compilerArgs.addAll(listOf(
"-Amapstruct.defaultComponentModel=spring"
))
}

Service Module:

// service/build.gradle.kts
plugins {
id("java-library")
}
dependencies {
// Project modules
api(project(":core"))
implementation(project(":shared:models"))
// Spring
implementation(libs.spring.boot.starter.validation)
// Utilities
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
implementation(libs.mapstruct)
annotationProcessor(libs.mapstruct.processor)
// Testing
testImplementation(libs.bundles.testing)
testImplementation(libs.spring.boot.starter.test)
}

Infrastructure Module:

// infrastructure/build.gradle.kts
plugins {
id("java-library")
alias(libs.plugins.spring.boot)
}
dependencies {
// Project modules
implementation(project(":core"))
implementation(project(":service"))
// Spring Cloud Config
implementation(libs.spring.cloud.starter.config)
implementation(libs.spring.cloud.starter.bootstrap)
// External services clients
implementation(libs.bundles.spring.boot.web)
// Resilience
implementation("io.github.resilience4j:resilience4j-spring-boot2:2.0.2")
implementation("io.github.resilience4j:resilience4j-circuitbreaker:2.0.2")
// Testing
testImplementation(libs.bundles.testing)
testImplementation(libs.testcontainers.junit.jupiter)
}

Shared Utils Module:

// shared/utils/build.gradle.kts
plugins {
id("java-library")
}
dependencies {
// Minimal dependencies for utility classes
implementation(libs.slf4j.api)
implementation(libs.guava)
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
// Testing
testImplementation(libs.junit.jupiter)
testImplementation(libs.mockito.core)
}
6. Custom Version Catalog for Internal Libraries
# gradle/internal.versions.toml - For internal/company libraries

[versions]

company-commons = "2.3.0" company-security = "1.5.0" company-monitoring = "3.1.0"

[libraries]

company-commons-core = { module = "com.company:commons-core", version.ref = "company-commons" } company-commons-utils = { module = "com.company:commons-utils", version.ref = "company-commons" } company-security-core = { module = "com.company:security-core", version.ref = "company-security" } company-monitoring-client = { module = "com.company:monitoring-client", version.ref = "company-monitoring" }

[bundles]

company-commons = ["company-commons-core", "company-commons-utils"] company-security = ["company-security-core"] company-monitoring = ["company-monitoring-client"]

Update settings.gradle.kts to include internal catalog:

dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("gradle/libs.versions.toml"))
}
create("companyLibs") {
from(files("gradle/internal.versions.toml"))
}
}
}

Usage in build files:

dependencies {
implementation(companyLibs.bundles.company.commons)
implementation(companyLibs.bundles.company.security)
}
7. Advanced Version Catalog Features

Platform Definitions:

// platform/build.gradle.kts - For BOM-style dependency management
plugins {
`java-platform`
}
dependencies {
constraints {
// Define all versions as constraints
api(libs.spring.boot.get())
api(libs.spring.cloud.get())
api(libs.junit.get())
// ... more constraints
}
}

Custom Catalog Access in Build Scripts:

// Accessing version catalog programmatically
tasks.register("printDependencyVersions") {
doLast {
println("=== Spring Boot Version ===")
println(libs.versions.spring.boot.get())
println("=== All Libraries ===")
libs.libraries.forEach { (name, dependency) ->
println("$name: ${dependency.version}")
}
println("=== Testing Bundle ===")
val testingBundle = libs.bundles.testing.get()
testingBundle.forEach { dependency ->
println(" - ${dependency.name}: ${dependency.version}")
}
}
}

Conditional Dependencies:

// Conditional dependency based on profile
val profile: String by project
dependencies {
if (profile == "dev") {
implementation(libs.h2)
} else {
implementation(libs.postgresql)
}
}
8. Migration Script from build.gradle

Before (Traditional build.gradle):

// build.gradle (old style)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:3.1.0'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.1.0'
implementation 'org.postgresql:postgresql:42.6.0'
implementation 'org.projectlombok:lombok:1.18.28'
annotationProcessor 'org.projectlombok:lombok:1.18.28'
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.3'
testImplementation 'org.mockito:mockito-core:5.3.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.0'
}

After (Version Catalog):

// build.gradle.kts (with version catalog)
dependencies {
implementation(libs.bundles.spring.boot.web)
implementation(libs.bundles.spring.data)
implementation(libs.postgresql)
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
testImplementation(libs.bundles.testing)
}
9. Dependency Verification
// build.gradle.kts - Dependency verification configuration
dependencyVerification {
verifyConfig = true
// Verify checksums for dependencies
checksums = mapOf(
"sha256" to listOf("warn", "fail-on-missing"),
"sha512" to listOf("warn")
)
}
// Task to validate dependency consistency
tasks.register("validateDependencies") {
doLast {
val catalog = the<VersionCatalogsExtension>().named("libs")
// Check for duplicate dependencies
val libraries = catalog.libraryNames
val duplicates = libraries.groupBy { it }.filter { it.value.size > 1 }
if (duplicates.isNotEmpty()) {
throw GradleException("Duplicate libraries found: $duplicates")
}
// Validate version consistency
catalog.libraryAliases.forEach { alias ->
val library = catalog.findLibrary(alias).get().get()
println("Validating: $alias -> ${library.module}:${library.version}")
}
println("Dependency validation completed successfully!")
}
}
10. Custom Tasks for Version Management
// Version management tasks
tasks.register("updateDependency", UpdateDependency::class) {
group = "versioning"
description = "Update a specific dependency version"
}
abstract class UpdateDependency : DefaultTask() {
@Option(option = "dependency", description = "Dependency to update")
var dependency: String = ""
@Option(option = "version", description = "New version")
var newVersion: String = ""
@TaskAction
fun update() {
if (dependency.isBlank() || newVersion.isBlank()) {
throw GradleException("Both --dependency and --version are required")
}
val catalogFile = project.file("gradle/libs.versions.toml")
val content = catalogFile.readText()
// Simple string replacement - in real implementation, use proper TOML parser
val updatedContent = content.replace(
"$dependency = \"[^\"]*\"".toRegex(),
"$dependency = \"$newVersion\""
)
catalogFile.writeText(updatedContent)
println("Updated $dependency to version $newVersion")
}
}
// Task to generate dependency report
tasks.register("dependencyReport", DependencyReport::class) {
group = "reporting"
description = "Generate comprehensive dependency report"
}
abstract class DependencyReport : DefaultTask() {
@OutputFile
val reportFile = project.layout.buildDirectory.file("reports/dependencies.md")
@TaskAction
fun generate() {
val catalog = the<VersionCatalogsExtension>().named("libs")
val report = buildString {
appendLine("# Dependency Report")
appendLine()
appendLine("## Versions")
catalog.versionAliases.forEach { alias ->
appendLine("- **$alias**: ${catalog.findVersion(alias).get().get()}")
}
appendLine()
appendLine("## Libraries")
catalog.libraryAliases.forEach { alias ->
val library = catalog.findLibrary(alias).get().get()
appendLine("- **$alias**: ${library.module}:${library.version}")
}
appendLine()
appendLine("## Bundles")
catalog.bundleAliases.forEach { alias ->
appendLine("- **$alias**:")
catalog.findBundle(alias).get().get().forEach { dependency ->
appendLine("  - ${dependency.name}: ${dependency.version}")
}
}
}
reportFile.get().asFile.writeText(report)
println("Dependency report generated: ${reportFile.get().asFile}")
}
}
11. Integration with CI/CD
# .github/workflows/dependency-check.yml
name: Dependency Check
on:
schedule:
- cron: '0 0 * * 1'  # Weekly
push:
branches: [ main ]
jobs:
dependency-updates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Check for dependency updates
run: ./gradlew dependencyUpdates -Drevision=release
- name: Validate dependencies
run: ./gradlew validateDependencies
12. Best Practices and Conventions

Naming Conventions:

# Good naming practices

[versions]

spring-boot = "3.1.0" # kebab-case for versions jackson = "2.15.2"

[libraries]

spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" } jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }

[bundles]

spring-boot-web = ["spring-boot-starter-web", "spring-boot-starter-validation"] testing = ["junit-jupiter", "mockito-core", "spring-boot-starter-test"]

Organization Strategies:

# Organize by domain/functionality

[versions]

# Core framework spring-boot = "3.1.0" # Database database-hibernate = "6.2.7" # Testing testing-junit = "5.9.3" # Group libraries by domain

[libraries]

# Web spring-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" } # Data spring-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa", version.ref = "spring-boot" } hibernate-core = { module = "org.hibernate.orm:hibernate-core", version.ref = "database-hibernate" } # Testing junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "testing-junit" }


Migration Strategy

  1. Start Small: Begin with a single module
  2. Incremental Migration: Convert one dependency group at a time
  3. Validation: Use dependency validation tasks
  4. Team Training: Ensure team understands new syntax
  5. CI Integration: Add validation to CI pipeline
// Migration helper task
tasks.register("migrateDependencies") {
doLast {
println("""
Migration Steps:
1. Create gradle/libs.versions.toml
2. Move versions to [versions] section
3. Define libraries in [libraries] section  
4. Create bundles for common groups
5. Update build.gradle.kts files
6. Test the build
7. Remove old version declarations
""".trimIndent())
}
}

Conclusion

Gradle Version Catalogs provide:

  • Centralized Management: Single source of truth for dependencies
  • Type Safety: IDE support and compile-time safety
  • Consistency: Uniform versions across projects
  • Maintainability: Easy updates and refactoring
  • Scalability: Suitable for large multi-module projects

By implementing version catalogs, you can significantly improve dependency management, reduce version conflicts, and streamline your build configuration across multiple Java projects.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper