Article 4: The Gradle Configuration Cache Trap
Published on 26 September 2025
Introduction
In our adventure of creating the plugin`site-baker`, we followed a rigorous TDD approach. Every feature was tested, validated, and we moved forward with confidence. And then, one day, the unexpected happened. Builds began to behave erratically. Changes in the plugin logic or in configuration files seemed to be ignored, and our functional tests, once reliable, were failing for no apparent reason.
This kind of problem can be incredibly frustrating. It calls into question the reliability of the tool and the validity of our code. After an intense debugging session, the culprit was identified: theGradle configuration cache.
The Symptom: Ghost Builds
The problem manifested in several ways:
-
I would modify a string in a task`println`, but the old string continued to appear during execution.
-
I would change a value in my`managed-jbake-context.yml`file, but the plugin acted as if the file had not been modified.
-
Functional tests, which create test projects on the fly, failed because the plugin did not seem to detect the freshly created configuration files.
Everything happened as if Gradle were executing a "ghost version" of our build, ignoring our most recent changes.
The Investigation: What is the Configuration Cache?
The configuration cache is a relatively modern and extremely powerful Gradle feature, enabled by default in newer versions. Its goal is to make builds faster.
-
During the first execution, Gradle runs theConfigurationphase (reading`build.gradle.kts`, creating tasks, resolving dependencies) and builds a task graph.
-
At the end of this phase, Gradleserializes this task graphand caches it.
-
During subsequent executions, if nothing has changed (build scripts,
gradle.properties, etc.), Gradlecompletely skips the configuration phaseand reuses the cached task graph.
The time gain is spectacular on large projects. However, this performance comes at a price: it imposes strict rules on how plugins must be written.
The Cause of the Problem: A Non-Compliant Plugin
Our plugin`site-baker`was unwittingly violating several configuration cache rules. For a task graph to be serializable, tasks must not contain references to complex objects like the`Project`object or read files arbitrarily during the execution phase.
Our main error was reading the content of the YAML file directly inside the task’s execution logic, using a reference to the path stored in our extension. This approach is incompatible with the cache because Gradle cannot know if the file content has changed if this read is not modeled as atask input(Task Input).
The Temporary Solution: Disabling the Cache
To unblock us and regain predictable build behavior, the fastest solution was to disable the configuration cache. Simply add the following line to the`gradle.properties`file of the project using the plugin (or in our case, the test project`site-baker`).
# site-baker/gradle.properties
org.gradle.configuration-cache=false
Instantly, the builds returned to their normal behavior. Each execution restarted the configuration phase and our changes were taken into account.
However, this is a workaround, not a sustainable solution. It sacrifices performance and does not solve the underlying problem of our plugin.
The Real Solution: Making the Plugin Compatible
For a plugin to be a good citizen of the modern Gradle ecosystem, it must be compatible with the configuration cache. This involves rethinking how data flows into our tasks.
The key is to use Gradle’sProvider APIs. Instead of passing direct values (such as a`String` ou un File) to our tasks, we must pass`Property<T>`or`Provider<T>`.
-
Declare task inputs:The task that parses the YAML file must declare this file as an input. For this, we use the`@InputFile`annotation.
[source,kotlin] ---- @get:InputFile abstract val configFile: RegularFileProperty ----
-
Use
PropertyandProvider:The value of`configFile`will be connected to the`configPath`property of our DSL extension. Gradle is thus able to track the provenance of the data.
[source,kotlin] ---- // In the plugin tasks.register<MyTask>("myTask") { configFile.set(extension.configPath.flatMap { project.layout.projectDirectory.file(it) }) } ----
-
Read the content at the right time:The file reading must take place inside the task action (
@TaskAction), using the`Provider`of the input.
[source,kotlin] ---- @TaskAction fun execute() { val content = configFile.get().asFile.readText() // … parse the content } ----
By following this model, Gradle understands that if the content of`configFile`changes, the configuration cache is invalid and the configuration phase must be re-executed. Furthermore, it knows that the task output depends on the content of this file, which also allows for the optimization of the execution cache (UP-TO-DATE).
Conclusion
This debugging adventure was a valuable lesson. The "magic" behavior of the configuration cache forced us to better understand the Gradle lifecycle and the principles of declarative and lazy programming (lazy).
Disabling the configuration cache is a useful diagnostic tool, but the real solution is to design robust and modern plugins. By correctly modeling our task inputs and outputs with the`Provider`APIs, we are not just fixing a bug; we are improving the performance, reliability, and maintainability of our plugin.
In the next article, we will put this refactoring into practice to make our YAML parsing task fully compatible with the configuration cache.