google-site-verification: google61fe8ba583a51912.html
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