General Gradle Best Practices
Use Kotlin DSL
Prefer the Kotlin DSL (build.gradle.kts
) over the Groovy DSL (build.gradle
) when authoring new builds or creating new subprojects in existing builds.
Explanation
The Kotlin DSL offers several advantages over the Groovy DSL:
-
Strict typing: IDEs provide better auto-completion and navigation with the Kotlin DSL.
-
Improved readability: Code written in Kotlin is often easier to follow and understand.
-
Single-language stack: Projects that already use Kotlin for production and test code don’t need to introduce Groovy just for the build.
Since Gradle 8.0, Kotlin DSL is the default for new builds created with gradle init
.
Android Studio also defaults to Kotlin DSL.
Use the Latest Minor Version of Gradle
Stay on the latest minor version of the major Gradle release you’re using, and regularly update your plugins to the latest compatible versions.
Explanation
Gradle follows a fairly predictable, time-based release cadence. Only the latest minor version of the current and previous major release is actively supported.
We recommend the following strategy:
-
Try upgrading directly to the latest minor version of your current major Gradle release.
-
If that fails, upgrade one minor version at a time to isolate regressions or compatibility issues.
Each new minor version includes:
-
Performance and stability improvements.
-
Deprecation warnings that help you prepare for the next major release.
-
Fixes for known bugs and security vulnerabilities.
Use the wrapper
task to update your project:
./gradlew wrapper --gradle-version <version>
Plugin Compatibility
Always use the latest compatible version of each plugin:
-
Upgrade Gradle before plugins.
-
Test plugin compatibility using shadow jobs.
-
Consult changelogs when updating.
Subscribe to the Gradle newsletter to stay informed about new Gradle releases, features, and plugins.
Apply Plugins Using the plugins
Block
You should always use the plugins
block to apply plugins in your build scripts.
Explanation
The plugins
block is the preferred way to apply plugins in Gradle.
The plugins API allows Gradle to better manage the loading of plugins and it is both more concise and less error-prone than adding dependencies to the buildscript’s classpath explicitly in order to use the apply
method.
It allows Gradle to optimize the loading and reuse of plugin classes and helps inform tools about the potential properties and values in extensions the plugins will add to the build script. It is constrained to be idempotent (produce the same result every time) and side effect-free (safe for Gradle to execute at any time).
Example
Don’t Do This
buildscript {
repositories {
gradlePluginPortal() (1)
}
dependencies {
classpath("com.google.protobuf:com.google.protobuf.gradle.plugin:0.9.4") (2)
}
}
apply(plugin = "java") (3)
apply(plugin = "com.google.protobuf") (4)
buildscript {
repositories {
gradlePluginPortal() (1)
}
dependencies {
classpath("com.google.protobuf:com.google.protobuf.gradle.plugin:0.9.4") (2)
}
}
apply plugin: "java" (3)
apply plugin: "com.google.protobuf" (4)
1 | Declare a Repository: To use the legacy plugin application syntax, you need to explicitly tell Gradle where to find a plugin. |
2 | Declare a Plugin Dependency: To use the legacy plugin application syntax with third-party plugins, you need to explicitly tell Gradle the full coordinates of the plugin. |
3 | Apply a Core Plugin: This is very similar using either method. |
4 | Apply a Third-Party Plugin: The syntax is the same as for core Gradle plugins, but the version is not present at the point of application in your buildscript. |
Do This Instead
plugins {
id("java") (1)
id("com.google.protobuf").version("0.9.4") (2)
}
plugins {
id("java") (1)
id("com.google.protobuf").version("0.9.4") (2)
}
1 | Apply a Core Plugin: This is very similar using either method. |
2 | Apply a Third-Party Plugin: You specify the version using method chaining in the plugins block itself. |
Do Not Use Internal APIs
Do not use APIs from a package where any segment of the package is internal
, or types that have Internal
or Impl
as a suffix in the name.
Explanation
Using internal APIs is inherently risky and can cause significant problems during upgrades. Gradle and many plugins (such as Android Gradle Plugin and Kotlin Gradle Plugin) treat these internal APIs as subject to unannounced breaking changes during any new Gradle release, even during minor releases. There have been numerous cases where even highly experienced plugin developers have been bitten by their usage of such APIs leading to unexpected breakages for their users.
If you require specific functionality that is missing, it’s best to submit a feature request. As a temporary workaround consider copying the necessary code into your own codebase and extending a Gradle public type with your own custom implementation using the copied code.
Example
Don’t Do This
import org.gradle.api.internal.attributes.AttributeContainerInternal
configurations.create("bad") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named<Usage>(Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>(Category.LIBRARY))
}
val badMap = (attributes as AttributeContainerInternal).asMap() (1)
logger.warn("Bad map")
badMap.forEach { (key, value) ->
logger.warn("$key -> $value")
}
}
import org.gradle.api.internal.attributes.AttributeContainerInternal
configurations.create("bad") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
}
def badMap = (attributes as AttributeContainerInternal).asMap() (1)
logger.warn("Bad map")
badMap.each {
logger.warn("${it.key} -> ${it.value}")
}
}
1 | Casting to AttributeContainerInternal and using toMap() should be avoided as it relies on an internal API. |
Do This Instead
configurations.create("good") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named<Usage>(Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named<Category>(Category.LIBRARY))
}
val goodMap = attributes.keySet().associate { (1)
Attribute.of(it.name, it.type) to attributes.getAttribute(it)
}
logger.warn("Good map")
goodMap.forEach { (key, value) ->
logger.warn("$key -> $value")
}
}
configurations.create("good") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
}
def goodMap = attributes.keySet().collectEntries {
[Attribute.of(it.name, it.type), attributes.getAttribute(it as Attribute<Object>)]
}
logger.warn("Good map")
goodMap.each {
logger.warn("$it.key -> $it.value")
}
}
1 | Implementing your own version of toMap() that only uses public APIs is a lot more robust. |
Modularize Your Builds
Modularize your builds by splitting your code into multiple projects.
Explanation
Splitting your build’s source into multiple Gradle projects (modules) is essential for leveraging Gradle’s automatic work avoidance and parallelization features. When a source file changes, Gradle only recompiles the affected projects. If all your sources reside in a single project, Gradle can’t avoid recompilation and won’t be able to run tasks in parallel. Splitting your source into multiple projects can provide additional performance benefits by minimizing each subproject’s compilation classpath and ensuring code generating tools such as annotation and symbol processors run only on the relevant files.
Do this soon. Don’t wait until you hit some arbitrary number of source files or classes to do this, instead structure your build into multiple projects from the start using whatever natural boundaries exist in your codebase.
Exactly how to best split your source varies with every build, as it depends on the particulars of that build. Here are some common patterns we found that can work well and make cohesive projects:
-
API vs. Implementation
-
Front-end vs. Back-end
-
Core business logic vs. UI
-
Vertical slices (e.g., feature modules each containing UI + business logic)
-
Inputs to source generation vs. their consumers
-
Or simply closely related classes.
Ultimately, the specific scheme matters less than ensuring that your build is split logically and consistently.
Expanding a build to hundreds of projects is common, and Gradle is designed to scale to this size and beyond. In the extreme, tiny projects containing only a class or two are probably counterproductive. However, you should typically err on the side of adding more projects rather than fewer.
Example
Don’t Do This
├── app // This project contains a mix of classes
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── CommonsUtil.java
│ └── GuavaUtil.java
│ └── Main.java
│ └── Util.java
├── settings.gradle.kts
├── app // This project contains a mix of classes
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── CommonsUtil.java
│ └── GuavaUtil.java
│ └── Main.java
│ └── Util.java
├── settings.gradle
include("app") (1)
include("app") (1)
plugins {
application (2)
}
dependencies {
implementation("com.google.guava:guava:31.1-jre") (3)
implementation("commons-lang:commons-lang:2.6")
}
application {
mainClass = "org.example.Main"
}
plugins {
id 'application' (2)
}
dependencies {
implementation 'com.google.guava:guava:31.1-jre' (3)
implementation 'commons-lang:commons-lang:2.6'
}
application {
mainClass = "org.example.Main"
}
1 | This build contains only a single project (in addition to the root project) that contains all the source code. If there is any change to any source file, Gradle will have to recompile and rebuild everything. While incremental compilation will help (especially in this simplified example) this is still less efficient then avoidance. Gradle also won’t be able to run any tasks in parallel, since all these tasks are in the same project, so this design won’t scale nicely. |
2 | As there is only a single project in this build, the application plugin must be applied here. This means that the application plugin will be affect all source files in the build, even those which have no need for it. |
3 | Likewise, the dependencies here are only needed by each particular implmentation of util. There’s no need for the implementation using Guava to have access to the Commons library, but it does because they are all in the same project. This also means that the classpath for each subproject is much larger than it needs to be, which can lead to longer build times and other confusion. |
Do This Instead
├── app
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── Main.java
├── settings.gradle.kts
├── util
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── Util.java
├── util-commons
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── CommonsUtil.java
└── util-guava
├── build.gradle.kts
└── src
└── main
└── java
└── org
└── example
└── GuavaUtil.java
├── app // App contains only the core application logic
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── Main.java
├── settings.gradle
├── util // Util contains only the core utility logic
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── Util.java
├── util-commons // One particular implementation of util, using Apache Commons
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ └── CommonsUtil.java
└── util-guava // Another implementation of util, using Guava
├── build.gradle
└── src
└── main
└── java
└── org
└── example
└── GuavaUtil.java
include("app") (1)
include("util")
include("util-commons")
include("util-guava")
include("app") (1)
include("util")
include("util-commons")
include("util-guava")
// This is the build.gradle file for the app module
plugins {
application (2)
}
dependencies { (3)
implementation(project(":util-guava"))
implementation(project(":util-commons"))
}
application {
mainClass = "org.example.Main"
}
// This is the build.gradle file for the app module
plugins {
id "application" (2)
}
dependencies { (3)
implementation project(":util-guava")
implementation project(":util-commons")
}
application {
mainClass = "org.example.Main"
}
// This is the build.gradle file for the util-commons module
plugins { (4)
`java-library`
}
dependencies { (5)
api(project(":util"))
implementation("commons-lang:commons-lang:2.6")
}
// This is the build.gradle file for the util-commons module
plugins { (4)
id "java-library"
}
dependencies { (5)
api project(":util")
implementation "commons-lang:commons-lang:2.6"
}
// This is the build.gradle file for the util-guava module
plugins {
`java-library`
}
dependencies {
api(project(":util"))
implementation("com.google.guava:guava:31.1-jre")
}
// This is the build.gradle file for the util-guava module
plugins {
id "java-library"
}
dependencies {
api project(":util")
implementation "com.google.guava:guava:31.1-jre"
}
1 | This build logically splits the source into multiple projects. Each project can be built independently, and Gradle can run tasks in parallel. This means that if you change a single source file in one of the projects, Gradle will only need to recompile and rebuild that project, not the entire build. |
2 | The application plugin is only applied to the app project, which is the only project that needs it. |
3 | Each project only adds the dependencies it needs. This means that the classpath for each subproject is much smaller, which can lead to faster build times and less confusion. |
4 | Each project only adds the specific plugins it needs. |
5 | Each project only adds the dependencies it needs. Projects can effectively use API vs. Implementation separation. |
Do Not Put Source Files in the Root Project
Do not put source files in your root project; instead, put them in a separate project.
Explanation
The root project is a special Project in Gradle that serves as the entry point for your build.
It is the place to configure some settings and conventions that apply globally to the entire build, that are not configured via Settings. For example, you can declare (but not apply) plugins here to ensure the same plugin version is consistently available across all projects and define other configurations shared by all projects within the build.
Be careful not to apply plugins unnecessarily in the root project - many plugins only affect source code and should only be applied to the projects that contain source code. |
The root project should not be used for source files, instead they should be located in a separate Gradle project.
Setting up your build like this from the start will also make it easier to add new projects as your build grows in the future.
Example
Don’t Do This
├── build.gradle.kts // Applies the `java-library` plugin to the root project
├── settings.gradle.kts
└── src // This directory shouldn't exist
└── main
└── java
└── org
└── example
└── MyClass1.java
├── build.gradle // Applies the `java-library` plugin to the root project
├── settings.gradle
└── src // This directory shouldn't exist
└── main
└── java
└── org
└── example
└── MyClass1.java
plugins { (1)
`java-library`
}
plugins {
id 'java-library' (1)
}
1 | The java-library plugin is applied to the root project, as there are Java source files are in the root project. |
Do This Instead
├── core
│ ├── build.gradle.kts // Applies the `java-library` plugin to only the `core` project
│ └── src // Source lives in a "core" (sub)project
│ └── main
│ └── java
│ └── org
│ └── example
│ └── MyClass1.java
└── settings.gradle.kts
├── core
│ ├── build.gradle // Applies the `java-library` plugin to only the `core` project
│ └── src // Source lives in a "core" (sub)project
│ └── main
│ └── java
│ └── org
│ └── example
│ └── MyClass1.java
└── settings.gradle
include("core") (1)
include("core") (1)
// This is the build.gradle.kts file for the core module
plugins { (2)
`java-library`
}
// This is the build.gradle file for the core module
plugins { (2)
id 'java-library'
}
1 | The root project exists only to configure the build, informing Gradle of a (sub)project named core . |
2 | The java-library plugin is only applied to the core project, which contains the Java source files. |
Set build flags in gradle.properties
Set Gradle build property flags in the gradle.properties
file.
Explanation
Instead of using command-line options or environment variables, set build flags in the root project’s gradle.properties
file.
Gradle comes with a long list of Gradle properties, which have names that begin with org.gradle
and can be used to configure the behavior of the build tool.
These properties can have a major impact on build performance, so it’s important to understand how they work.
You should not rely on supplying these properties via the command-line for every Gradle invocation.
Providing these properties via the command line is intended for short-term testing and debugging purposes, but it’s prone to being forgotten or inconsistently applied across environments.
A permanent, idiomatic location to set and share these properties is in the gradle.properties
file located in the root project directory.
This file should be added to source control in order to share these properties across different machines and between developers.
You should understand the default values of the properties your build uses and avoid explicitly setting properties to those defaults. Any change to a property’s default value in Gradle will follow the standard deprecation cycle, and users will be properly notified.
Properties set this way are not inherited across build boundaries when using composite builds. |
Example
Don’t Do This
├── build.gradle.kts
└── settings.gradle.kts
├── build.gradle
└── settings.gradle
tasks.register("first") {
doLast {
throw GradleException("First task failing as expected")
}
}
tasks.register("second") {
doLast {
logger.lifecycle("Second task succeeding as expected")
}
}
tasks.register("run") {
dependsOn("first", "second")
}
tasks.register("first") {
doLast {
throw new GradleException("First task failing as expected")
}
}
tasks.register("second") {
doLast {
logger.lifecycle("Second task succeeding as expected")
}
}
tasks.register("run") {
dependsOn("first", "second")
}
This build is run with gradle run -Dorg.gradle.continue=true
, so that the failure of the first
task does not prevent the second
task from executing.
This relies on person running the build to remember to set this property, which is error prone and not portable across different machines and environments.
Do This Instead
├── build.gradle.kts
└── gradle.properties
└── settings.gradle.kts
├── build.gradle
└── gradle.properties
└── settings.gradle
org.gradle.continue=true
This build sets the org.gradle.continue
property in the gradle.properties
file.
Now it can be executed using only gradle run
, and the continue property will always be set automatically across all environments.
Favor build-logic
Composite Builds for Build Logic
You should setup a Composite Build (often called an "included build") to hold your build logic—including any custom plugins, convention plugins, and other build-specific customizations.
Explanation
The preferred location for build logic is an included build (typically named build-logic
), not in buildSrc
.
The automatically available buildSrc
is great for rapid prototyping, but it comes with some subtle disadvantages:
-
There are classloader differences in how these 2 approaches behave that can be surprising; included builds are treated just like external dependencies, which is a simpler mental model. Dependency resolution behaves subtly differently in
buildSrc
. -
There can potentially be fewer task invalidations in a build when files in an included build are modified, leading to faster builds. Any change in
buildSrc
causes the entire build to become out-of-date, whereas changes in a subproject of an included build only cause projects in the build using the products of that particular subproject to be out-of-date. -
Included builds are complete Gradle builds and can be opened, worked on, and built independently as standalone projects. It is straightforward to publish their products, including plugins, in order to share them with other projects.
-
The
buildSrc
project automatically applies thejava
plugin, which may be unnecessary.
One important caveat to this recommendation is when creating Settings
plugins.
Defining these in a build-logic
project requires it to be included in the pluginManagement
block of the main build’s settings.gradle(.kts)
file, in order to make these plugins available to the build early enough to be applied to the Settings
instance.
This is possible, but reduces Build Caching capability, potentially impacting performance.
A better solution is to use a separate, minimal, included build (e.g. build-logic-settings
) to hold only Settings
plugins.
Another potential reason to use buildSrc
is if you have a very large number of subprojects within your included build-logic
.
Applying a different set of build-logic
plugins to the subprojects in your including build will result in a different classpath being used for each.
This may have performance implications and make your build harder to understand.
Using different plugin combinations can cause features like Build Services to break in difficult to diagnose ways.
Ideally, there would be no difference between using buildSrc
and an included build, as buildSrc
is intended to behave like an implicitly available included build.
However, due to historical reasons, these subtle differences still exist.
As this changes, this recommendation may be revised in the future.
For now, these differences can introduce confusion.
Since setting up a composite build requires only minimal additional configuration, we recommend using it over buildSrc
in most cases.
Example
Don’t Do This
├── build.gradle.kts
├── buildSrc
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ ├── MyPlugin.java
│ └── MyTask.java
└── settings.gradle.kts
├── build.gradle
├── buildSrc
│ ├── build.gradle
│ └── src
│ └── main
│ └── java
│ └── org
│ └── example
│ ├── MyPlugin.java
│ └── MyTask.java
└── settings.gradle
// This file is located in /buildSrc
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "org.example.myplugin"
implementationClass = "org.example.MyPlugin"
}
}
}
// This file is located in /buildSrc
plugins {
id "java-gradle-plugin"
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "org.example.myplugin"
implementationClass = "org.example.MyPlugin"
}
}
}
Set up a Plugin Build: This is the same using either method.
rootProject.name = "favor-composite-builds"
rootProject.name = "favor-composite-builds"
buildSrc
products are automatically usable: There is no additional configuration with this method.
Do This Instead
├── build-logic
│ ├── plugin
│ │ ├── build.gradle.kts
│ │ └── src
│ │ └── main
│ │ └── java
│ │ └── org
│ │ └── example
│ │ ├── MyPlugin.java
│ │ └── MyTask.java
│ └── settings.gradle.kts
├── build.gradle.kts
└── settings.gradle.kts
├── build-logic
│ ├── plugin
│ │ ├── build.gradle
│ │ └── src
│ │ └── main
│ │ └── java
│ │ └── org
│ │ └── example
│ │ ├── MyPlugin.java
│ │ └── MyTask.java
│ └── settings.gradle
├── build.gradle
└── settings.gradle
// This file is located in /build-logic/plugin
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "org.example.myplugin"
implementationClass = "org.example.MyPlugin"
}
}
}
// This file is located in /build-logic/plugin
plugins {
id "java-gradle-plugin"
}
gradlePlugin {
plugins {
create("myPlugin") {
id = "org.example.myplugin"
implementationClass = "org.example.MyPlugin"
}
}
}
Set up a Plugin Build: This is the same using either method.
// This file is located in the root project
includeBuild("build-logic") (1)
rootProject.name = "favor-composite-builds"
// This file is located in the root project
includeBuild("build-logic") (1)
rootProject.name = "favor-composite-builds"
// This file is located in /build-logic
rootProject.name = "build-logic"
include("plugin") (2)
// This file is located in /build-logic
rootProject.name = "build-logic"
include("plugin") (2)
1 | Composite builds must be explicitly included: Use the includeBuild method to locate and include a build in order to use its products. |
2 | Structure your included build into subprojects: This allows the main build to only depend on the necessary parts of the included build. |