Gradle Version Catalog is a powerful feature that allows you to centralize dependency versions, plugins, and libraries across multiple modules and projects. It provides type-safe dependency declarations, improves build consistency, and simplifies dependency management in large Java projects.
What is Gradle Version Catalog?
Version Catalog enables you to:
- Define dependencies centrally in a single location
- Share versions across multiple modules
- Enable type-safe access in build scripts
- Improve maintainability of dependency declarations
- Standardize dependency names across teams
Traditional vs Version Catalog Approach
Traditional:
// app/build.gradle dependencies { implementation 'org.springframework.boot:spring-boot-starter-web:3.2.0' implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.2.0' testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.0' } Version Catalog:
// app/build.gradle dependencies { implementation libs.spring.boot.starter.web implementation libs.spring.boot.starter.data.jpa testImplementation libs.spring.boot.starter.test } Hands-On Tutorial: Implementing Gradle Version Catalog
Let's build a complete multi-module Java project with comprehensive version catalog configuration.
Step 1: Project Structure Setup
multi-module-project/ ├── gradle/ │ └── libs.versions.toml # Version catalog definition ├── build-logic/ │ ├── build.gradle.kts │ └── src/main/groovy/ │ └── java-common-conventions.gradle ├── core/ │ └── build.gradle.kts ├── service/ │ └── build.gradle.kts ├── web/ │ └── build.gradle.kts ├── build.gradle.kts └── settings.gradle.kts
Step 2: Version Catalog Definition
gradle/libs.versions.toml:
[versions] # Spring Boot spring-boot = "3.2.0" spring-dependency-management = "1.1.4" # Spring Framework spring-core = "6.1.1" spring-context = "6.1.1" spring-data-jpa = "3.2.0" spring-security = "6.2.0" # Database hibernate = "6.4.1" postgresql = "42.7.1" h2 = "2.2.224" flyway = "10.5.0" # Testing junit = "5.10.1" mockito = "5.8.0" assertj = "3.24.2" testcontainers = "1.19.3" # Logging slf4j = "2.0.9" logback = "1.4.14" # JSON jackson = "2.16.1" # HTTP Client okhttp = "4.12.0" retrofit = "2.9.0" # Monitoring micrometer = "1.12.0" micrometer-tracing = "1.2.0" # Utilities guava = "32.1.3-jre" apache-commons = "3.14.0" vavr = "0.10.4" # Build Tools lombok = "1.18.30" mapstruct = "1.5.5.Final" # Code Quality spotless = "6.23.0" checkstyle = "10.12.5" pmd = "6.55.0" jacoco = "0.8.11"
[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-security = { module = "org.springframework.boot:spring-boot-starter-security", 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 Framework spring-core = { module = "org.springframework:spring-core", version.ref = "spring-core" } spring-context = { module = "org.springframework:spring-context", version.ref = "spring-context" } spring-data-jpa = { module = "org.springframework.data:spring-data-jpa", version.ref = "spring-data-jpa" } # Database hibernate-core = { module = "org.hibernate.orm:hibernate-core", version.ref = "hibernate" } postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } h2 = { module = "com.h2database:h2", version.ref = "h2" } flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flyway" } # 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" } assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" } testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" } # Logging slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } # JSON 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" } # HTTP Client okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } # Monitoring micrometer-core = { module = "io.micrometer:micrometer-core", version.ref = "micrometer" } micrometer-registry-prometheus = { module = "io.micrometer:micrometer-registry-prometheus", version.ref = "micrometer" } micrometer-tracing = { module = "io.micrometer:micrometer-tracing", version.ref = "micrometer-tracing" } # Utilities guava = { module = "com.google.guava:guava", version.ref = "guava" } apache-commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "apache-commons" } vavr = { module = "io.vavr:vavr", version.ref = "vavr" } # Build Tools 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" } # Code Quality spotless-plugin-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" } jacoco = { id = "com.diffplug.gradle.spotless", version.ref = "jacoco" }
[bundles]
# Common dependency bundles spring-web = ["spring-boot-starter-web", "spring-boot-starter-validation"] spring-data = ["spring-boot-starter-data-jpa", "hibernate-core"] testing = ["junit-jupiter", "mockito-core", "assertj-core", "mockito-junit-jupiter"] monitoring = ["micrometer-core", "micrometer-registry-prometheus", "micrometer-tracing"] database = ["postgresql", "flyway-core", "h2"]
[metadata]
format.version = "1.1"
Step 3: Settings Configuration
settings.gradle.kts:
rootProject.name = "multi-module-project" // Enable version catalog enableFeaturePreview("VERSION_CATALOGS") // Define version catalog dependencyResolutionManagement { versionCatalogs { create("libs") { from(files("gradle/libs.versions.toml")) } } } // Include build logic includeBuild("build-logic") // Project modules include(":core") include(":service") include(":web") include(":integration-tests") project(":core").projectDir = file("core") project(":service").projectDir = file("service") project(":web").projectDir = file("web") project(":integration-tests").projectDir = file("integration-tests") Step 4: Build Logic Convention Plugin
build-logic/build.gradle.kts:
plugins { `groovy-gradle-plugin` } repositories { gradlePluginPortal() mavenCentral() } dependencies { implementation(libs.plugins.spring.boot.get().toString()) implementation(libs.plugins.spring.dependency.management.get().toString()) implementation(libs.plugins.jacoco.get().toString()) } build-logic/src/main/groovy/java-common-conventions.gradle.kts:
plugins { java jacoco checkstyle } repositories { mavenCentral() } java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 withSourcesJar() withJavadocJar() } dependencies { // Common dependencies for all Java modules implementation(libs.slf4j.api) implementation(libs.apache.commons.lang3) // Testing bundle testImplementation(libs.bundles.testing) } tasks.withType<Test> { useJUnitPlatform() testLogging { events("passed", "failed", "skipped") setExceptionFormat("full") } finalizedBy(tasks.jacocoTestReport) } tasks.jacocoTestReport { dependsOn(tasks.test) reports { xml.required.set(true) html.required.set(true) csv.required.set(false) } } checkstyle { toolVersion = libs.versions.checkstyle.get() configFile = rootProject.file("config/checkstyle/checkstyle.xml") isIgnoreFailures = false maxErrors = 0 maxWarnings = 0 } tasks.withType<Checkstyle> { reports { xml.required.set(false) html.required.set(true) } } Step 5: Root Build Configuration
build.gradle.kts:
plugins { id("java-common-conventions") } allprojects { group = "com.example" version = "1.0.0-SNAPSHOT" repositories { mavenCentral() maven { url = uri("https://repo.spring.io/milestone") } } } subprojects { apply(plugin = "java-common-conventions") dependencies { // Common dependencies for all subprojects constraints { implementation(libs.guava) implementation(libs.vavr) } } } // Code quality tasks tasks.register("codeQuality") { group = "verification" description = "Runs all code quality checks" dependsOn(subprojects.map { it.tasks.named("check") }) } tasks.register("testAll") { group = "verification" description = "Runs tests in all modules" dependsOn(subprojects.map { it.tasks.named("test") }) } // JaCoCo aggregate report tasks.register<JacocoReport>("jacocoRootReport") { group = "verification" description = "Generates an aggregate code coverage report" executionData(fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec")) subprojects.forEach { subproject -> subproject.plugins.withType<JacocoPlugin>().configureEach { subproject.the<JacocoPluginExtension>().apply { subproject.tasks.matching { it.extensions.findByType<JacocoTaskExtension>() != null } .configureEach { sourceDirectories.from(files(classDirectories)) } classDirectories.from(files(classDirectories)) } } } reports { html.required.set(true) xml.required.set(true) csv.required.set(false) } } Step 6: Core Module Configuration
core/build.gradle.kts:
plugins { id("java-library") } description = "Core domain models and business logic" dependencies { api(libs.spring.core) api(libs.spring.context) implementation(libs.hibernate.core) implementation(libs.jackson.databind) implementation(libs.jackson.datatype.jsr310) // Annotation processors compileOnly(libs.lombok) annotationProcessor(libs.lombok) compileOnly(libs.mapstruct) annotationProcessor(libs.mapstruct.processor) // Test dependencies testImplementation(libs.testcontainers.junit.jupiter) testImplementation(libs.testcontainers.postgresql) } tasks.jar { manifest { attributes( "Implementation-Title" to project.name, "Implementation-Version" to project.version ) } } Step 7: Service Module Configuration
service/build.gradle.kts:
plugins { id("java-library") id("org.springframework.boot") apply false id("io.spring.dependency-management") } description = "Business service layer implementation" dependencyManagement { imports { mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) } } dependencies { api(project(":core")) implementation(libs.bundles.spring.data) implementation(libs.bundles.monitoring) implementation(libs.bundles.database) implementation(libs.spring.boot.starter.actuator) implementation(libs.spring.boot.starter.validation) // Annotation processors compileOnly(libs.lombok) annotationProcessor(libs.lombok) // Runtime dependencies runtimeOnly(libs.postgresql) runtimeOnly(libs.h2) // Test dependencies testImplementation(libs.spring.boot.starter.test) testImplementation(libs.bundles.testing) testRuntimeOnly(libs.logback.classic) } tasks.test { useJUnitPlatform() systemProperty("spring.profiles.active", "test") } Step 8: Web Module Configuration
web/build.gradle.kts:
plugins { id("java") id("org.springframework.boot") id("io.spring.dependency-management") } description = "Web layer with REST API" dependencies { implementation(project(":core")) implementation(project(":service")) implementation(libs.bundles.spring.web) implementation(libs.spring.boot.starter.security) implementation(libs.spring.boot.starter.actuator) implementation(libs.bundles.monitoring) implementation(libs.okhttp) implementation(libs.retrofit) // Annotation processors compileOnly(libs.lombok) annotationProcessor(libs.lombok) compileOnly(libs.mapstruct) annotationProcessor(libs.mapstruct.processor) // Test dependencies testImplementation(libs.spring.boot.starter.test) testImplementation(libs.bundles.testing) testImplementation(libs.spring.security.test) } springBoot { buildInfo() } tasks.bootJar { manifest { attributes( "Start-Class" to "com.example.web.Application", "Spring-Boot-Version" to libs.versions.spring.boot.get() ) } } tasks.test { useJUnitPlatform() maxHeapSize = "2G" } Step 9: Integration Tests Module
integration-tests/build.gradle.kts:
plugins { id("java") } description = "Integration tests module" dependencies { implementation(project(":web")) implementation(project(":service")) implementation(libs.spring.boot.starter.web) implementation(libs.spring.boot.starter.test) implementation(libs.bundles.testing) implementation(libs.testcontainers.junit.jupiter) implementation(libs.testcontainers.postgresql) testImplementation(libs.spring.security.test) } tasks.test { useJUnitPlatform() systemProperty("spring.profiles.active", "integration-test") testLogging { events("passed", "failed", "skipped") showStandardStreams = true } } Step 10: Custom Version Catalog Extensions
gradle/dependencies.gradle.kts:
import org.gradle.accessors.dm.LibrariesForLibs // Extension functions for common dependency patterns val LibrariesForLibs.`spring-boot-bom` get() = plugins.spring.boot.get() fun LibrariesForLibs.springBootStarter(starter: String) = library("spring-boot-starter-$starter", "org.springframework.boot:spring-boot-starter-$starter") fun LibrariesForLibs.testingBundle() = bundles.testing fun LibrariesForLibs.databaseBundle() = bundles.database // Version accessors val LibrariesForLibs.javaVersion get() = JavaVersion.VERSION_17 // Dependency constraints helper fun DependencyHandler.constraints(configure: DependencyConstraintHandler.() -> Unit) { (this as ExtensionAware).extensions.configure("constraints", configure) } Step 11: Advanced Version Catalog with Plugins
gradle/plugins.versions.toml:
[versions] spring-boot = "3.2.0" jacoco = "0.8.11" spotless = "6.23.0" sonarqube = "4.4.1.3373" git-properties = "2.4.1" docker = "9.4.0"
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.4" } jacoco = { id = "jacoco", version.ref = "jacoco" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" } git-properties = { id = "com.gorylenko.gradle-git-properties", version.ref = "git-properties" } docker = { id = "com.bmuschko.docker-spring-boot-application", version.ref = "docker" }
Step 12: Build Script Using Version Catalog
build.gradle.kts (using extensions):
plugins { alias(libs.plugins.spring.boot) apply false alias(libs.plugins.spring.dependency.management) apply false alias(libs.plugins.jacoco) alias(libs.plugins.spotless) alias(libs.plugins.sonarqube) } apply(from = "gradle/dependencies.gradle.kts") allprojects { apply(plugin = "jacoco") apply(plugin = "com.diffplug.spotless") spotless { java { target("src/**/*.java") googleJavaFormat(libs.versions.spotless.get()) removeUnusedImports() trimTrailingWhitespace() endWithNewline() } kotlin { target("src/**/*.kt") ktlint(libs.versions.spotless.get()) trimTrailingWhitespace() endWithNewline() } } } sonarqube { properties { property("sonar.projectKey", "multi-module-project") property("sonar.host.url", "http://localhost:9000") property("sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml") } } // Custom task for dependency updates tasks.register("dependencyUpdates", com.github.benmanes.gradle.versions.tasks.DependencyUpdatesTask::class) { group = "help" description = "Displays dependency updates" resolutionStrategy { componentSelection { all { if (isNonStable(candidate.version) && !isNonStable(currentVersion)) { reject("Release candidate") } } } } outputFormatter = "html" outputDir = "build/dependencyUpdates" } fun isNonStable(version: String): Boolean { val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) } val regex = "^[0-9,.v-]+(-r)?$".toRegex() val isStable = stableKeyword || regex.matches(version) return !isStable } Step 13: Domain Models (Example Usage)
core/src/main/java/com/example/core/domain/Product.java:
package com.example.core.domain; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import javax.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; @Data @Builder @NoArgsConstructor @AllArgsConstructor @Entity @Table(name = "products") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String sku; @Column(nullable = false) private String name; private String description; @Column(nullable = false) private BigDecimal price; private Integer stockQuantity; @Enumerated(EnumType.STRING) private ProductStatus status; @Column(nullable = false) private LocalDateTime createdAt; private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } public enum ProductStatus { ACTIVE, INACTIVE, DISCONTINUED } } Step 14: Service Implementation
service/src/main/java/com/example/service/ProductService.java:
package com.example.service; import com.example.core.domain.Product; import com.example.core.repository.ProductRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @Slf4j @Service @RequiredArgsConstructor @Transactional public class ProductService { private final ProductRepository productRepository; public Product createProduct(Product product) { log.info("Creating product: {}", product.getSku()); return productRepository.save(product); } @Transactional(readOnly = true) public Optional<Product> getProductById(Long id) { return productRepository.findById(id); } @Transactional(readOnly = true) public List<Product> getAllActiveProducts() { return productRepository.findByStatus(Product.ProductStatus.ACTIVE); } public Product updateProduct(Long id, Product productDetails) { return productRepository.findById(id) .map(existingProduct -> { existingProduct.setName(productDetails.getName()); existingProduct.setDescription(productDetails.getDescription()); existingProduct.setPrice(productDetails.getPrice()); existingProduct.setStockQuantity(productDetails.getStockQuantity()); return productRepository.save(existingProduct); }) .orElseThrow(() -> new ProductNotFoundException("Product not found with id: " + id)); } public void deleteProduct(Long id) { productRepository.deleteById(id); log.info("Deleted product with id: {}", id); } public static class ProductNotFoundException extends RuntimeException { public ProductNotFoundException(String message) { super(message); } } } Step 15: Running the Build
Build commands:
# Build all modules ./gradlew build # Run tests with coverage ./gradlew testAll jacocoRootReport # Check code quality ./gradlew codeQuality # Check for dependency updates ./gradlew dependencyUpdates # Build specific module ./gradlew :web:build # Run spotless formatting ./gradlew spotlessApply # Sonar analysis ./gradlew sonarqube
Step 16: CI/CD Integration
.github/workflows/build.yml:
name: Java CI with Gradle on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: 'gradle' - name: Build with Gradle run: ./gradlew build - name: Run tests with coverage run: ./gradlew testAll jacocoRootReport - name: Code quality checks run: ./gradlew codeQuality - name: Upload coverage reports uses: codecov/codecov-action@v3 with: file: ./build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml
Best Practices
1. Naming Conventions
# Good naming spring-boot-starter-web = "org.springframework.boot:spring-boot-starter-web" jackson-databind = "com.fasterxml.jackson.core:jackson-databind" # Avoid web = "org.springframework.boot:spring-boot-starter-web" # Too vague jackson = "com.fasterxml.jackson.core:jackson-databind" # Ambiguous
2. Organization Strategies
- Group by functionality: spring, database, testing, etc.
- Use bundles for commonly used dependency groups
- Separate versions from library definitions
- Use meaningful aliases that match library names
3. Maintenance Tips
- Regularly update versions using
dependencyUpdates - Use version ranges cautiously in libraries
- Document major version changes
- Test thoroughly after version updates
Benefits of Version Catalog
- Centralized Management: Single source of truth for dependencies
- Type Safety: IDE support and compile-time checking
- Consistency: Uniform dependency declarations across modules
- Maintainability: Easy version updates and dependency changes
- Team Collaboration: Standardized dependency naming
Conclusion
Gradle Version Catalog transforms dependency management in Java projects by providing:
- Centralized version control across multiple modules
- Type-safe dependency declarations with IDE support
- Consistent dependency naming conventions
- Improved maintainability through bundles and conventions
- Better team collaboration with standardized approaches
By implementing Version Catalog in your Java projects, you can significantly reduce build configuration complexity, prevent version conflicts, and create a more maintainable and scalable build system that grows with your project's needs.