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.