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
- Start Small: Begin with a single module
- Incremental Migration: Convert one dependency group at a time
- Validation: Use dependency validation tasks
- Team Training: Ensure team understands new syntax
- 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.