Gradle Lockfile for Multi-Project Builds

INFO: Japanese version is available in zenn.dev.


Gradle allows you to manage dependency library versions by specifying version ranges. This mechanism is known as dynamic versions.

To ensure reproducible builds, however, it is necessary to lock these version ranges to specific fixed versions. Lock files such as npm’s package-lock.json and Rust’s Cargo.lock are well known examples of dependency management. Gradle provides a similar mechanism called dependency locking, which generates a lock file named gradle.lockfile.

Although dependency locking has been supported since Gradle 4.8, it is still not widely used. This is partly due to the lack of clear guidance on integrating dependency locking with complex project setup, including multi-project builds, buildSrc, and version catalogs libs.versions.toml.

One does not simply generate a Gradle.lockfile for multi-project builds

Gradle: Generating Multi-Project Lockfiles1

In this article, we will show how to introduce Gradle’s dependency locking into projects structured with multi-project builds and buildSrc.

Preparation

This walkthrough uses Gradle 8.14.2.

The sample project is available on GitHub at ajalab/gradle-lockfile-for-multi-projects, based on its initial commit 5e67156. It consists of two Kotlin subprojects, app1 and app2, along with a buildSrc build script. The buildSrc directory provides a convention plugin, kotlin-jvm.gradle.kts, which both subprojects use to share common build logic.

All configuration files are written in the Gradle Kotlin DSL (*.kts), and most of them were adapted from the ones generated by IntelliJ IDEA’s “Generate multi-module build” option.

Initially, the project specifies Kotlin version 2.1.0. Later, we’ll switch it to a dynamic version to confirm that dependency locking correctly applies the upgrade.

Adding Dependencies

Commit: 43ae29f

First, we add the Spring Boot Gradle Plugin (org.springframework.boot) and the Spring Boot Starter (org.springframework.boot:spring-boot-starter) as dependencies for both app1 and app2.

diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3ff2f2c..320b016 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,6 +4,11 @@

 [versions]
 kotlin = "2.1.0"
+springBoot = "3.3.13"

 [libraries]
 kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+springBootStarter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springBoot" }
+
+[plugins]
+springBootGradlePlugin = { id = "org.springframework.boot", version.ref = "springBoot" }
diff --git a/app1/build.gradle.kts b/app1/build.gradle.kts
index b0fd2d0..c0ace6f 100644
--- a/app1/build.gradle.kts
+++ b/app1/build.gradle.kts
@@ -1,6 +1,8 @@
 plugins {
     id("buildsrc.convention.kotlin-jvm")
+    alias(libs.plugins.springBootGradlePlugin)
 }

 dependencies {
+    implementation(libs.springBootStarter)
 }
diff --git a/app2/build.gradle.kts b/app2/build.gradle.kts
index b0fd2d0..c0ace6f 100644
--- a/app2/build.gradle.kts
+++ b/app2/build.gradle.kts
@@ -1,6 +1,8 @@
 plugins {
     id("buildsrc.convention.kotlin-jvm")
+    alias(libs.plugins.springBootGradlePlugin)
 }

 dependencies {
+    implementation(libs.springBootStarter)
 }

At this point, we specify Spring Boot version 3.3.13. Later, we’ll switch to a dynamic version to confirm that dependency locking correctly applies the upgrade.

Enabling Dependency Locking

Commit: 8ff1725

To enable dependency locking, call lockAllConfigurations 2 inside the dependencyLocking section of your build script.

dependencyLocking {
    lockAllConfigurations()
}

Notably, dependency locking must be configured for each subproject and also for buildSrc. In our example, this requires changes in two places:

Generating the Lock File

Commit: 4aff070

The lock file gradle.lockfile records the fixed versions of dependencies, including all transitive dependencies.

app1/gradle.lockfile

# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
ch.qos.logback:logback-classic:1.5.18=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
ch.qos.logback:logback-core:1.5.18=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.github.java-diff-utils:java-diff-utils:4.12=kotlinInternalAbiValidation
...

To generate the lock file, run a Gradle task with the --write-locks option.

In a multi-project Gradle setup, lock files are generated for each subproject as well as for buildSrc. A common pitfall is that while the official documentation shows running gradle dependencies --write-locks at the root project, this does not generate lock files for the subprojects3.

In a multi-project setup, note that dependencies is executed only on one project, typically the root project.

Locking Versions

One workaround is to run the dependencies task individually for each subproject.

gradle :buildSrc:dependencies --write-locks
gradle :app1:dependencies --write-locks
gradle :app2:dependencies --write-locks

However, specifying each subproject individually can be cumbersome. We will look at two methods to simplify it.

Using the build Task

Running the dependencies task without specifying project paths targets only the root project, so lock files won’t be generated for subprojects. Instead, by using a task like build that runs across all subprojects, you can generate lock files for all subprojects in a single command execution.

gradle build --write-locks

Defining a Task to Run the dependencies Task for All Subprojects

Using the build task has the downside of triggering an actual build, which is unnecessary just for generating lock files.

As an alternative, you can define a custom task that depends on the dependencies task of all subprojects. Below is an example of defining such a task named allDependencies4.

build.gradle.kts

tasks.register("allDependencies") {
    dependsOn(
        subprojects.flatMap { subproject ->
            subproject.tasks.matching { it.name == "dependencies" }
        }
    )
}

By running this allDependencies task with the --write-locks option, you can generate lock files for all subprojects in a single step.

gradle allDependencies --write-locks

Specifying Version Ranges

Commit: d8df924

With the lock files generated, we can now specify version ranges for Kotlin and Spring Boot using dynamic versions. Version ranges can be set directly in the versions section of the version catalog.

gradle/libs.versions.toml

[versions]
kotlin = "2.+"
springBoot = "3.+"

The + symbol acts as a prefix descriptor for version matching.

At this point, since the lock files haven’t been updated yet, running the build will still use the old versions (kotlin = "2.1.0" and springBoot = "3.3.13").

Updating the Lock File

To update the lock file, rerun the dependencies task (or similar) with the --write-locks option, just like when you initially generated it.

After running this, the lock file will reflect the updated versions (kotlin = "2.2.20" and springBoot = "3.5.4").

diff --git i/buildSrc/gradle.lockfile w/buildSrc/gradle.lockfile
index fc38189..1f0a988 100644
--- i/buildSrc/gradle.lockfile
+++ w/buildSrc/gradle.lockfile
@@ -1,29 +1,32 @@
 # This is a Gradle generated file for dependency locking.
 # Manual edits can break the build and are not advised.
 # This file is expected to be part of source control.

...

+org.jetbrains.kotlin:kotlin-compiler-runner:2.2.20-Beta2=buildScriptClasspath,runtimeClasspath
 org.jetbrains.kotlin:kotlin-daemon-client:2.0.21=kotlinBuildToolsApiClasspath
-org.jetbrains.kotlin:kotlin-daemon-client:2.1.0=buildScriptClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-daemon-client:2.2.20-Beta2=buildScriptClasspath,runtimeClasspath
 org.jetbrains.kotlin:kotlin-daemon-embeddable:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin-api:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:2.1.0=buildScriptClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin-idea:2.1.0=buildScriptClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin-model:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugins-bom:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-klib-commonizer-api:2.1.0=buildScriptClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-native-utils:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin-api:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:2.2.20-Beta2=buildScriptClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin-idea:2.2.20-Beta2=buildScriptClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin-model:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugins-bom:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-klib-commonizer-api:2.2.20-Beta2=buildScriptClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-native-utils:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath

...
diff --git i/app1/gradle.lockfile w/app1/gradle.lockfile
index 1e2875b..81d8baa 100644
--- i/app1/gradle.lockfile
+++ w/app1/gradle.lockfile
@@ -3,49 +3,55 @@
 # This file is expected to be part of source control.

...

 org.slf4j:slf4j-api:2.0.17=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework.boot:spring-boot-autoconfigure:3.3.13=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework.boot:spring-boot-starter-logging:3.3.13=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework.boot:spring-boot-starter:3.3.13=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework.boot:spring-boot:3.3.13=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-aop:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-beans:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-context:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-core:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-expression:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-jcl:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.yaml:snakeyaml:2.2=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework.boot:spring-boot-autoconfigure:3.5.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter-logging:3.5.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter:3.5.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework.boot:spring-boot:3.5.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-aop:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-beans:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-context:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-core:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-expression:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-jcl:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.yaml:snakeyaml:2.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,test
ImplementationDependenciesMetadata,testRuntimeClasspath

Summary

In this article, we demonstrated how to introduce dependency locking into a Gradle project structured with a multi-project build.


  1. An example of introducing Gradle dependency locking can be found in the Elasticsearch project (elastic/elasticsearch). ↩︎

  2. To enable dependency locking for specific dependency configurations, such as compileClasspath, call activateDependencyLocking on the target configuration within the configurations block. See Activate locking for specific configurations for details. ↩︎

  3. The issue where the dependencies task does not generate lock files for subprojects is discussed in gradle/gradle#9373. Unfortunately, the fix is currently not planned↩︎

  4. The idea originates from a comment in gradle/gradle#9373↩︎