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. | 
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.
Name Your Root Project
Always name your root project in the settings.gradle(.kts) file.
Explanation
While an empty settings.gradle(.kts) file is enough to create a multi-project build, you should always set the rootProject.name property.
By default, the root project’s name is taken from the directory containing the build. This can be problematic if the directory name contains spaces, Gradle logical path separators, or other special characters. It also makes task paths dependent on the directory name, rather than being reliably defined.
Explicitly setting the root project’s name ensures consistency across environments. Project names appear in error messages, logs, and reports, and builds often run on different machines, such as CI servers. Builds may execute on a variety of machines or environments, such as CI servers, and should report the same root project name anywhere to make the project more comprehensible.
Example
Don’t Do This
// Left empty
// Left empty
In this build, the settings file is empty and the root project has no explicit name.
Running the projects report shows that Gradle assigns an implicit name to the root project, derived from the build’s current directory.
Unfortunately that name varies based on where the project currently lives.
For example, if the project is checked out into a directory named some-directory-name, the output of ./gradlew projects will look like this:
> Task :projects
Projects:
------------------------------------------------------------
Root project 'some-directory-name'
------------------------------------------------------------
Do This Instead
rootProject.name = "my-example-project"
rootProject.name = "my-example-project"
In this build, the root project is explicitly named.
The explicit name my-example-project will be used in all reports, logs, and error messages.
Regardless of where the project lives, the output of ./gradlew projects will look like this:
> Task :projects
Projects:
------------------------------------------------------------
Root project 'my-example-project'
------------------------------------------------------------
Project hierarchy:
Root project 'my-example-project'
No sub-projects
To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :tasks
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Do not use gradle.properties in subprojects
Do not place a gradle.properties file inside subprojects to configure your build.
Explanation
Gradle allows gradle.properties files in both the root project and subprojects, but support for subproject properties is inconsistent.
Gradle itself and many popular plugins (such as the Android Gradle Plugin and Kotlin Gradle Plugin) do not reliably handle this pattern.
Using subproject gradle.properties files also makes it harder to understand and debug your build.
Property values may be scattered across multiple locations, overridden in unexpected ways, or difficult to trace back to their source.
If you need to set properties for a single subproject, define them directly in that subproject’s build.gradle(.kts).
If you need to apply properties across multiple subprojects, extract the configuration into a convention plugin.
Example
Don’t Do This
├── app
│   ├── ⋮
│   ├── build.gradle.kts
│   └── gradle.properties
├── utilities
│   ├── ⋮
│   ├── build.gradle.kts
│   └── gradle.properties
└── settings.gradle.kts
├── app
│   ├── ⋮
│   ├── build.gradle
│   └── gradle.properties
├── utilities
│   ├── ⋮
│   ├── build.gradle
│   └── gradle.properties
└── settings.gradle
# This file is located in /app
propertyA=fixedValue
propertyB=someValue
// This file is located in /app
tasks.register("printProperties") { (1)
    val propA = project.properties.get("propertyA") (2)
    val propB = project.properties.get("propertyB")
    doLast {
        println("propertyA in app: $propA")
        println("propertyB in app: $propB")
    }
}
// This file is located in /app
tasks.register("printProperties") { (1)
    def propA = project.properties.get("propertyA") (2)
    def propB = project.properties.get("propertyB")
    doLast {
        println "propertyA in app: $propA"
        println "propertyB in app: $propB"
    }
}
# This file is located in /util
propertyA=fixedValue
propertyB=otherValue
// This file is located in /util
tasks.register("printProperties") {
    val propA = project.properties.get("propertyA")
    val propB = project.properties.get("propertyB") (3)
    doLast {
        println("propertyA in util: $propA")
        println("propertyB in util: $propB")
    }
}
// This file is located in /util
tasks.register("printProperties") {
    def propA = project.properties.get("propertyA")
    def propB = project.properties.get("propertyB") (3)
    doLast {
        println "propertyA in util: $propA"
        println "propertyB in util: $propB"
    }
}
| 1 | Register a task that uses the value of properties in each subproject. | 
| 2 | The task reads properties, which are supplied by the project-local app/gradle.properties file.  propertyA does not vary between subprojects. | 
| 3 | 'util’s print task reads the properties which are supplied by util/gradle.properties.  propertyB varies between subprojects. | 
This structure requires duplicating properties that are shared between subprojects and is not guaranteed to remain supported.
Do This Instead
├── buildSrc
│   └──  ⋮
├── app
│   ├── ⋮
│   └── build.gradle.kts
├── utilities
│   ├── ⋮
│   └── build.gradle.kts
├── settings.gradle.kts
└── gradle.properties
├── buildSrc
│   └──  ⋮
├── app
│   ├── ⋮
│   └──  build.gradle
├── utilities
│   ├── ⋮
│   └── build.gradle
├── settings.gradle
└── gradle.properties
# This file is located in the root of the build
propertyA=fixedValue
propertyB=someValue
import org.gradle.api.provider.Property
interface ProjectProperties { (1)
    val propertyA: Property<String>
    val propertyB: Property<String>
}
import org.gradle.api.provider.Property
interface ProjectProperties { (1)
    Property<String> getPropertyA()
    Property<String> getPropertyB()
}
extensions.create<ProjectProperties>("myProperties") (2)
tasks.register("printProperties") { (3)
    val myProperties = project.extensions.getByName("myProperties") as ProjectProperties
    val projectName = project.name
    doLast {
        println("propertyA in ${projectName}: ${myProperties.propertyA.get()}")
        println("propertyB in ${projectName}: ${myProperties.propertyB.get()}")
    }
}
extensions.create("myProperties", ProjectProperties) (2)
tasks.register("printProperties") { (3)
    def myProperties = project.extensions.getByName("myProperties") as ProjectProperties
    def projectName = project.name
    doLast {
        println("propertyA in ${projectName}: ${myProperties.propertyA.get()}")
        println("propertyB in ${projectName}: ${myProperties.propertyB.get()}")
    }
}
// This file is located in /app
plugins { (4)
    id("project-properties")
}
myProperties { (5)
    propertyA = project.properties.get("propertyA") as String
    propertyB = project.properties.get("propertyB") as String
}
// This file is located in /app
plugins { (4)
    id "project-properties"
}
myProperties { (5)
    propertyA = project.properties.get("propertyA")
    propertyB = project.properties.get("propertyB")
}
// This file is located in /util
plugins {
    id("project-properties")
}
myProperties {
    propertyA = project.properties.get("propertyA") as String
    propertyB = "otherValue" (6)
}
// This file is located in /util
plugins {
    id "project-properties"
}
myProperties {
    propertyA = project.properties.get("propertyA")
    propertyB = "otherValue" (6)
}
| 1 | Define a simple extension type in buildSrc to hold property values. | 
| 2 | Register that property in a convention plugin. | 
| 3 | Register tasks using property values in the convention plugin. | 
| 4 | Apply the convention plugin in each subproject. | 
| 5 | Set the extension’s property values in each subproject’s build script.  This uses the values defined in the root gradle.properties file.  The task reads values from the extension, not directly from the project properties. | 
| 6 | When values need to vary between subprojects, they can be set directly on the extension. | 
This structure uses an extension type to hold values, allowing properties to be strongly typed, and for property values and operations on properties to be defined in a single location. Overriding values per subproject remains straightforward.