Use UTF-8 File Encoding

Set UTF-8 as the default file encoding to ensure consistent behavior across platforms.

Explanation

Use UTF-8 as the default file encoding to ensure consistent behavior across environments and avoid caching issues caused by platform-dependent default encodings.

This is especially important when working with build caching, since differences in file encoding between environments can cause unexpected cache misses.

To enforce UTF-8 encoding, add the following to your gradle.properties file:

org.gradle.jvmargs=-Dfile.encoding=UTF-8
Do not rely on the default encoding of the underlying JVM or operating system, as this may differ between environments and lead to inconsistent behavior.

Use the Build Cache

Use the Build Cache to save time by reusing outputs produced by previous builds.

Explanation

The Build Cache avoids re-executing tasks when their inputs haven’t changed by reusing outputs from previous builds.

This prevents redundant work. If the inputs are the same, the outputs will be too, resulting in faster, more efficient builds.

Example

Don’t Do This

Build caching is disabled by default:

gradle.properties
# caching is off by default
# org.gradle.caching=false

Do This Instead

To enable the Build Cache, add the following to your gradle.properties file:

gradle.properties
org.gradle.caching=true

When you build your project for the first time, Gradle populates the cache with the outputs of tasks like compilation.

Even if you run ./gradlew clean to delete the build directory, Gradle can reuse cached outputs in subsequent builds.

> ./gradlew clean
:clean
BUILD SUCCESSFUL

On subsequent builds, instead of executing the :compileJava task again, the outputs of the task will be loaded from the Build Cache:

> ./gradlew compileJava
> Task :compileJava FROM-CACHE

BUILD SUCCESSFUL in 0s
1 actionable task: 1 from cache

Use the Configuration Cache

Use the Configuration Cache to significantly improve build performance by caching the result of the configuration phase and reusing it in subsequent builds.

Explanation

The Configuration Cache works by saving the result of the configuration phase. On the next build, if nothing relevant has changed, Gradle skips configuration entirely and loads the cached task graph from disk, jumping straight to task execution.

This can dramatically reduce build time for large builds, but it’s just as valuable for smaller builds where configuration overhead can dominate short iterations. Faster feedback helps developers stay focused, without waiting on redundant configuration work.

It’s important to understand how this differs from the Build Cache. The Build Cache stores the outputs of task execution, while the Configuration Cache stores the configured task graph before execution begins. These are independent mechanisms that solve different problems, but they are designed to work together for optimal performance.

The Configuration Cache is the preferred way to execute Gradle builds, but it is not enabled by default. Many existing builds and plugins are not yet fully compatible, and adopting it may involve refactoring of build logic. Enabling it by default could lead to unexpected build failures, so Gradle uses an opt-in adoption model to allow teams to verify compatibility and adopt configuration caching incrementally and safely.

Example

Don’t Do This

Configuration Caching is not enabled by default:

gradle.properties
# caching is off by default
# org.gradle.configuration-cache=false

Do This Instead

To enable the Configuration Cache, add the following to your gradle.properties file:

gradle.properties
org.gradle.configuration-cache=true

When you build your project for the first time, Gradle stores the outcome of the configuration phase, including the task graph, in the Configuration Cache.

> ./gradlew compileJava

Configuration cache entry stored.
> Task :processResources NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :compileJava
> Task :classes
> Task :compileTestJava NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :jar
> Task :assemble
> Task :build

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

On subsequent builds, instead of reconfiguring tasks like :compileJava, Gradle loads the task graph from the Configuration Cache and proceeds directly to execution.

> ./gradlew compileJava

Configuration cache entry reused.
> Task :processResources NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :compileJava
> Task :classes
> Task :compileTestJava NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :jar
> Task :assemble
> Task :build

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

Avoid Expensive Computations in Configuration Phase

Avoid expensive computations in the configuration phase, instead, move them to task actions.

Explanation

In order for Gradle to execute tasks it first needs to build the project task graph. As part of discovering what tasks to include in the task graph, Gradle will configure all the tasks that are directly requested, any task dependencies of the requested tasks, and also any tasks that are not lazily registered. This work is done in the configuration phase.

Performing expensive or slow operations such as file or network I/O, or CPU-heavy calculations in the configuration phase forces these to run even when they might be unnecessary to complete the requested work of the invoked tasks. It is better to move these operations to task actions so that they run only when required.

Example

Don’t Do This

build.gradle.kts
abstract class MyTask : DefaultTask() {
    @get:Input
    lateinit var computationResult: String
    @TaskAction
    fun run() {
        logger.lifecycle(computationResult)
    }
}

fun heavyWork(): String {
    println("Start heavy work")
    Thread.sleep(5000)
    println("Finish heavy work")
    return "Heavy computation result"
}

tasks.register<MyTask>("myTask") {
    computationResult = heavyWork() (1)
}
build.gradle
abstract class MyTask extends DefaultTask {
    @Input
    String computationResult
    @TaskAction
    void run() {
        logger.lifecycle(computationResult)
    }
}

String heavyWork() {
    logger.lifecycle("Start heavy work")
    Thread.sleep(5000)
    logger.lifecycle("Finish heavy work")
    return "Heavy computation result"
}

tasks.register("myTask", MyTask) {
    computationResult = heavyWork() (1)
}
1 Performing heavy computation during configuration phase.

Do This Instead

build.gradle.kts
abstract class MyTask : DefaultTask() {
    @TaskAction
    fun run() {
        logger.lifecycle(heavyWork()) (1)
    }

    fun heavyWork(): String {
        logger.lifecycle("Start heavy work")
        Thread.sleep(5000)
        logger.lifecycle("Finish heavy work")
        return "Heavy computation result"
    }
}

tasks.register<MyTask>("myTask")
build.gradle
abstract class MyTask extends DefaultTask {
    @TaskAction
    void run() {
        logger.lifecycle(heavyWork()) (1)
    }
    String heavyWork() {
        logger.lifecycle("Start heavy work")
        Thread.sleep(5000)
        logger.lifecycle("Finish heavy work")
        return "Heavy computation result"
    }
}

tasks.register("myTask", MyTask)
1 Performing heavy computation during execution phase in a task action.