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
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:
- The convention plugin used by the subprojects to share build logic–
kotlin-jvm.gradle.kts - The build script for
buildSrcitself–buildSrc/build.gradle.kts
Generating the Lock File
Commit: 4aff070
The lock file gradle.lockfile records the fixed versions of dependencies, including all transitive dependencies.
# 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
dependenciesis executed only on one project, typically the root project.
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.
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.
[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.
An example of introducing Gradle dependency locking can be found in the Elasticsearch project (elastic/elasticsearch). ↩︎
To enable dependency locking for specific dependency configurations, such as
compileClasspath, callactivateDependencyLockingon the target configuration within theconfigurationsblock. See Activate locking for specific configurations for details. ↩︎The issue where the
dependenciestask does not generate lock files for subprojects is discussed in gradle/gradle#9373. Unfortunately, the fix is currently not planned. ↩︎The idea originates from a comment in gradle/gradle#9373. ↩︎