Unifying Library Version Management with Gradle Version Catalogs

Hello! I’m Hirotaka Kawata (@hktechno), and I work as a Kotlin Expert at MoneyForward, providing development support for Kotlin backend projects.

MoneyForward currently has approximately 80 Kotlin/Java backend project repositories (we have even more Ruby and Go projects).

As the number of repositories in an organization grows, one major challenge that emerges is managing different types of frameworks and libraries, along with their respective versions consistently.

In this article, I’d like to share our approach to unified library version management for Kotlin/Java backend development using Gradle Version Catalogs.

The Importance of Library Version Management

As projects multiply, each one ends up adding various libraries as dependencies.

However, as organizations grow larger, several problems start to emerge.

High costs when updating frameworks and libraries
Constantly managing library updates across each project is expensive and resource-intensive. Because of these high costs, libraries often don’t get updated and become outdated. Even with tools like Dependabot or Renovate, compatibility issues mean it’s rarely a straightforward process. When updating frameworks like Spring Boot, many dependencies (including transitive ones) get updated simultaneously, but the testing and verification work is still extremely challenging.

Security and safety concerns
Managing which projects use which libraries at what versions (SBOM – Software Bill of Materials) is difficult. When issues arise with specific libraries, without unified usage tracking, investigation methods, and upgrade approaches, it takes much longer to respond. Individual project investigations can’t thoroughly check every corner, so safety isn’t guaranteed.

To address these problems, we believe unified library version management across the organization is crucial.

Most of our Kotlin/Java backend projects use Gradle as their build system. So we decided to build a library version management approach using Gradle.

By using Gradle Version Catalogs to provide a shared library version catalog, we get benefits like:

  • Access to a company-standard library catalog that’s already been safety tested and validated
  • Users can update many library versions at once by updating just one catalog version
  • When issues arise with specific libraries, it becomes easy to check whether projects are affected

Gradle Version Catalogs

Have you been using Gradle’s Version Catalogs?

This feature was introduced in Gradle 7, allowing you to declaratively define dependency libraries, plugins, and their versions, then reference them from build scripts.

Previously, we achieved shared version management through build script workarounds like loading Maven BOMs with platform() or using Spring Boot’s Dependency Management Plugin. However, Gradle now provides version catalogs as a built-in feature, and it’s recommended as a best practice for Kotlin development.

Basic Usage

There are several ways to define and use version catalogs, but the most basic approach is defining dependencies in libs.versions.toml and referencing them from build.gradle.kts.

gradle/libs.versions.toml

[versions]
mockk = "1.14.2"

[libraries]
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }

...

build.gradle.kts

dependencies {
    // Using version catalog
    implementation(libs.mockk)
    // Without version catalog
    implementation("io.mockk:mockk:1.14.2")
}
This is the simplest form. The feature also has other useful capabilities like defining Gradle plugins in catalogs and creating bundles - groups of dependencies that can all be added to build scripts at once.

Version catalog information works with IDE completion, so if you define libraries in the catalog beforehand, you don’t need to search for package and artifact names every time.

Publishing Version Catalogs

You can publish defined version catalogs to Maven repositories and share them across projects.

By creating a module in Gradle with the version-catalog plugin, you can generate a version catalog for publishing within that module and publish it using the same process as regular libraries.

The basic configuration for publishing a version catalog defined in libs.versions.toml looks like this:

build.gradle.kts

plugins {
    `version-catalog`
    `maven-publish`
}

group = "com.moneyforward.gradle"

catalog {
    versionCatalog {
        from(files("gradle/libs.versions.toml"))
    }
}

publishing {
    publications {
        create<MavenPublication>("mavenVersionCatalog") {
            from(components["versionCatalog"])
        }
    }
    repositories {
        ...
    }
}
To read this published catalog from other projects, you need configuration like this:

settings.gradle.kts

dependencyResolutionManagement {
    repositories {
        maven("https://<YOUR_MAVEN_REPO>")
    }
    versionCatalogs {
        create("mylibs") {
            from("com.moneyforward.gradle:catalog:1.0.0")
        }
    }
}

With this configuration, you can reference dependencies from the shared version catalog using the format mylibs.mockk.

Each project can maintain its own libs.versions.toml separate from the common one, so you can manage project-specific dependencies if needed (though this reduces the benefits since library usage becomes harder to track).

This achieves our minimum goals.

Resolving BOMs with the version-catalog-generator Plugin

However, when actually operating version catalogs, the following management drawbacks become apparent:

  • Libraries that frameworks like Spring Boot depend on should use the same versions as the framework, but this is difficult to manage (otherwise things break)
  • It’s difficult to describe all artifacts provided by specific libraries in the version catalog
    • Example: List of Spring Boot Starters
    • New ones get added later, and managing them all is challenging
    • While you could use BOMs provided by each library and Gradle’s Platform feature to manage version dependencies, this reduces the advantages of version catalogs

To solve these problems, we decided to use the version-catalog-generator Gradle plugin to import Maven BOMs.

The version-catalog-generator plugin resolves all transitive dependencies included in a specified BOM into a version catalog. It also handles BOMs included within BOMs.

For Spring Boot, a Maven BOM is provided called org.springframework.boot:spring-boot-dependencies.

By resolving this BOM using version-catalog-generator as shown below, all dependencies included in Spring Boot become part of the version catalog. This plugin is really helpful.

libs.versions.toml

[versions]
springBoot = "3.5.5"

[libraries]
springBootDependencies = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "springBoot" }

settings.gradle.kts

dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
    versionCatalogs {
        generate("mylibs") {
            fromToml("springBootDependencies")
        }
    }
}
build.gradle.kts
dependencies {
    implementation(mylibs.spring.springBootStarterWebflux)
    implementation(mylibs.h2database.h2)
}
However, BOMs only get resolved in projects that include the version-catalog-generator plugin - you can't publish version catalogs with flattened BOM dependencies. This is because the version catalog publishing module configuration shown earlier only specifies TOML files via from(files(...)).

While you could introduce version-catalog-generator to all projects, given the complexity, this isn’t very realistic either. Ideally, we want to publish version catalogs with flattened BOM dependencies.

Publishing Version Catalogs with Flattened BOMs

We looked for a way to copy and publish the contents of version catalogs currently loaded in Gradle projects.

We solved this with a custom build script approach. Since it uses reflection, this approach isn’t officially supported and may break in future versions, but it dynamically creates version catalogs using DSL within build scripts based on currently loaded version catalogs.

This allowed us to successfully publish version catalogs with flattened BOMs.

build.gradle.kts

catalog {
    versionCatalog {
        // Copy the version catalog from the `mylibs`.
        val config = (mylibs::class as KClass<AbstractExternalDependencyFactory>)
            .memberProperties.first { it.name == "config" }
            .let {
                it.isAccessible = true
                it.get(mylibs) as DefaultVersionCatalog
            }

        config.versionAliases.forEach {
            version(it, config.getVersion(it).version.displayName)
        }

        config.libraryAliases.forEach {
            val dependency = config.getDependencyData(it)
            library(it, dependency.group, dependency.name).also { library ->
                if (dependency.versionRef != null) {
                    library.versionRef(dependency.versionRef!!)
                } else {
                    library.version(dependency.version.displayName)
                }
            }
        }
...
I've published an example of successfully publishing version catalogs with flattened BOMs in the following repository. Please use it as a reference:

https://github.com/hktechn0/shared-gradle-version-catalog-example

Differences Between Maven BOM and Gradle Version Catalogs

After reading this far, many of you might think “Wouldn’t traditional Maven BOMs be better than the new Gradle Version Catalogs?”

Indeed, Gradle Version Catalogs and Maven BOMs have many similarities. Generally, when providing library version sets within companies or in libraries, Maven BOMs have been widely used.

However, if you’re using only Gradle for builds, I believe version catalogs are superior to BOMs in the following ways:

  • Manageable with simple TOML
  • IDE completion works when specifying dependencies
  • Can include Gradle plugin dependencies
  • It’s Gradle’s official version management approach, with expectations for future improvements and feature additions

Even with version catalogs, you can apply automatic library updates via Renovate to both dependencies within catalogs and catalog updates themselves. While this may require additional configuration for private Maven repositories in some cases, it’s not a major issue.

Summary and Current Status

We’re implementing the approach described in this article at MoneyForward’s Kotlin projects, publishing shared version catalogs generated through this method to our internal Maven repository to standardize library version management.

By providing commonly used frameworks and libraries in a single version catalog, development teams can eliminate the hassle of constantly chasing detailed library version updates.

However, introducing this to all existing developed projects at once is challenging, so we’re gradually implementing it.

Looking ahead to the end of 2025, major updates including Java 25 and Spring Boot 4 are waiting for us. If we can apply version catalogs to many projects before these updates, we believe we can significantly reduce update costs across multiple projects.

Published-date