Common Gradle misconceptions

rolgalan
8 min readOct 7, 2023

--

Initially it looks like a big arrow pointing to the left. But if you look closer it is composed by a group of smaller arrows that are actually pointing to the right.

How I thought Gradle behaves, and what it actually does

Gradle, the build automation tool, is quite versatile. It works with multiple languages and allows you to add any type of customization you require to control the process of compilation, testing, and deployment in your projects.

In order to support this flexibility, Gradle has grown over time, becoming more and more complex. It provides multiple mechanisms forconfiguring its behavior in the best way to suit your requirements.

I had a lot of misconceptions that were proven false as I kept using it over the years and learning more about it. Talking with other people I noticed that I was not the only one with these confusions, and that many of these misunderstandings are quite common in the industry.

In this article we are going to review some of these common Gradle fallacies and explain the actual behavior.

The Gradle Daemon does everything

Let’s start with the first misconception, probably the most trivial. At the beginning I thought that the Gradle Daemon was the only process required to build a project… until I opened a JVM memory profiler and noticed that there are multiple processes involved in a build.

Screenshot of VisualVm. List of multiple ongoing processes named as: GradleWrapperMain, Gradle Daemon, KotlinCompileDaemon and 6 different instances of GradleWorkerMain.

The first one is GradleWrapperMain, the Gradle client JVM, which is not mentioned frequently due its low memory footprint and importance. It’s the actual entry point for Gradle. Its duty is basically to search for compatible daemons to execute the build logic and forward inputs/outputs between the daemon and the console.

But there are more. If your build needs to compile Java or Kotlin code it will create separate Java and Kotlin compiler daemons (see the KotlinCompileDaemon above). By the way, the Java compiler was a single-purpose process that used to be shut down at the end of each build. This changed with the recent release of Gradle 8.3, which promoted this disposable worker to a long-lived daemon. This allows successive builds to reuse this warmed-up process to go faster.

When you execute tests, Gradle will start some GradleWorkerMain processes to launch your test suites. This is done by default to ensure test isolation by preventing classpath pollution (and to avoid excessive memory footprint in the build process). In general you will get a separate process for each module test requested, but this can be configured to behave differently through different settings (for example, forking multiple processes in parallel for each module, or by spawning a new one each certain amount of tests).

Besides the tests, your build might contain other tasks using the WorkerApi and configured to use process isolation. These tasks will be executed in separate Gradle worker daemons as well.

Gradle daemon disabled won’t create a daemon

Let’s focus now on the Gradle Daemon.

You can enable/disable it by adding the following flag into your gradle.properties file org.gradle.daemon=(true,false). Or by adding this command-line flag --daemon, --no-daemon. This is enabled by default.

It might look obvious that if you set daemon to false there would be no daemon, right? Nope! Gradle will still spawn a JVM process to execute all your tasks, and this is precisely the Gradle daemon. However, this process is disposable and will be stopped at the end of the build, as it is a single-use daemon.

There is only one situation where this separate disposable daemon won’t be created. If JAVA_OPTS and GRADLE_OPTS match org.gradle.jvmargs, the Daemon will not be used at all since the build happens inside the Gradle client JVM mentioned in the previous section.

Without parallel execution enabled you can’t have parallel tasks

Parallel execution is going to be fun on different levels, just wait.

You can use the Gradle property org.gradle.parallel=(true,false) or the command line flags --parallel, --no-parallel. This is disabled by default.

If you don’t enable this flag you would expect all your tasks to be executed in serial in a single process, right? This is incorrect again.

This flag instructs Gradle to execute tasks of independent subprojects/modules in parallel, usually in multiple threads inside the Gradle Daemon.

However, even if you have the parallel flag st to false, you can still see some tasks executed in parallel (even in different JVM processes). How is that possible? Because in your build you might have tasks that implement the Worker API, which provides the ability to execute these tasks’ work concurrently and asynchronously.

This happened to me recently to me in one project: in the timeline I saw multiple tasks being executed in parallel so I assumed this flag was enabled. Until one day, reviewing other configurations, I noticed this project was missing the parallel flag and it blew my mind until I learnt the reason.

The next two pictures represent the same build from the same project. The first one is with the parallel flag disabled, and you can still see some parallel tasks here and there. However, once parallel is enabled, many more tasks are now executed in parallel as expected (each from different subprojects).

Timeline graph of two builds for the same task. The left one contains some parallel tasks. On the right there are way more, fully leveraging parallelization.

Configuration cache just caches configuration

Configuration cache side effects are another astonishing learning from this year.

You can use the Gradle property org.gradle.configuration-cache=(true,false), and the command line flag--configuration-cache,-- no-configuration-cache. This is disabled by default.

Its purpose is obvious, this will simply cache the build configuration, so it can be reused in further executions and avoid all the time spent during the configuration stage. Right? Partially wrong again. It does cache the configuration indeed… but the Configuration Cache enables many other performance improvements!

The most impressive one is that… it allows parallel execution of tasks inside each project (as long as they are independent, of course).

Wait. We already said that we have the Worker API and the parallel flag mentioned above. How is this different? The parallel flag allows inter-module parallelization, whereas the configuration cache allows intra-module parallelization. On top of this, the Worker API also enables specific tasks to be executed in parallel regardless of these flags as mentioned before.

Enabling configuration cache requires all your tasks to be defined in such a way that they avoid side-effects during their execution. I believe that this “strict-mode” gives enough confidence to Gradle to execute these tasks in parallel when they are not depending on each other.

The next two pictures represent the same build from the same project, targeting a specific task from a submodule. The first one executes with configuration cache disabled, and the second one enabled. In this particular example, it allows tasks from different variants to run in parallel.

Timeline graph of two builds for the same task. The left one runs mostly everything in serial. On the right there are way more, fully leveraging parallelization.

Gradle stop kills all daemons

Probably everyone knows that you can run gradle --status to list all existing daemons, and gradle --stop to kill all them.

However, there is one important caveat: it will only list or kill those daemons with the same version as the current version of Gradle executing this command.

It might happen that on your local machine you have multiple projects, each of them using a different Gradle version. So you won’t interact with other versions of Gradle when running these commands.

There are many other more reliable options to get the Gradle processes in your machine, For example pgrep -fli gradle. But I usually run jps as it lists all java processes, and at the end Gradle runs inside jvm processes.

gradle.properties is inherited by composite builds

I recognize that this might not be such a common misconception, but it was enough for me to raise an issue to Gradle until they confirmed it was expected, so I’m adding it to this list 😅.

In Gradle you can have multiple independent projects connected together through composite builds. In case you are not familiar with this concept, these are projects that can be built/developed independently, but that are dependencies from some other main root project (these are different from submodule/subprojects, which are not equally independent). buildSrc is probably the best known example of this pattern.

When you build a project using composite builds, these internal projects are implicitly built as well, before your main project.

One might think then that all your settings declared in your gradle.properties file in your root project are inherited by the rest of your projects (if they do not set any overriding configuration). However, this is not the reality, as “included builds do not share any configuration with the composite build, or the other included builds. Each included build is configured and executed in isolation”. So in the absence of specific configuration the composite builds rely on Gradle defaults and not the values used by your main project configuration.

Final words

Considering that it is the base automation tool that orchestrates all your builds, it is fundamental to understand how it works in order to leverage it and get the best of it. Due its adaptability, Gradle can be more complex than it appears at first glance. Especially if you never dived deeply into it and relied on others configuring the project for you. I am constantly learning something new about Gradle in the process of optimizing the build time of different projects.

We might have many wrong intuitions about how Gradle works, as happens with any tool. However, Gradle documentation is usually quite complete: almost everything mentioned in this article points to the actual parts of the documentation where it is explained (but these sections might not always be noticed). Some others are easily verified by running a local experiment and observing what happens with your build. In case of doubt, always review the documentation or run a simple test to confirm your hypothesis. If you cannot figure something out yourself, you can join the amazing Gradle Community Slack, where many great minds could help you. I personally solved some of the misconceptions from this article there (thanks to everyone selflessly helping others!).

Which other Gradle misconceptions did you used to have? I would love to know yours!

--

--