skip to content

You're migrating a build to the configuration cache and the report flags many 'reading project property' violations. How do you fix property reads using the providers API, and what subtle behavior changes should you watch for?

level: seniorimportance: should knowfreq 30%

answer

  1. project.property/getProperty/getenv → providers.*
  2. wire lazily + .orElse defaults
  3. findProperty null → provider absent
  4. reads shift config→execution time
  5. property becomes tracked cache input

basics

~10 s

Replace project.property/findProperty and System.getProperty/getenv reads with providers.gradleProperty/systemProperty/environmentVariable, wired lazily into task inputs. Watch that absence now stays lazy and that values are read at execution, not configuration, time.

solid answer

~40 s

The configuration cache forbids touching `Project` (and `System.getProperty`/`getenv` at execution from cached tasks). The mechanical fix is a source swap: - `project.property("x")` / `project.findProperty("x")` → `providers.gradleProperty("x")` - `System.getProperty("x")` → `providers.systemProperty("x")` - `System.getenv("X")` → `providers.environmentVariable("X")` Then **wire** each provider into a `Property`/task input (with `.orElse(...)` for defaults) rather than resolving it eagerly. Subtle changes to watch: (1) reads move from configuration time to execution time, so values resolved very early may shift; (2) absent properties no longer throw immediately — you must add explicit `.orElse`/`.getOrElse` where old code relied on `findProperty` returning null or `property` throwing; (3) the property becomes a tracked cache input, so changing it now correctly invalidates the cache (sometimes surprising teams used to stale behavior). The payoff is a build whose property reads are reproducible and cacheable.

code

kotlin · 9 lines
kotlin
// Before (config-cache violations)
// val flag = project.findProperty("flag") ?: "off"
// val token = System.getenv("DEPLOY_TOKEN") ?: ""

// After: lazy, cache-safe, wired into inputs
tasks.register<DeployTask>("deploy") {
    flag.set(providers.gradleProperty("flag").orElse("off"))
    token.set(providers.environmentVariable("DEPLOY_TOKEN").orElse(""))
}

go deeper

for a junior

Know the basic source swap from project.property to providers.gradleProperty.

for a middle

Map all three legacy sources to providers and wire them with defaults.

for a senior

Reason about timing shift, absence semantics, and new cache-input invalidation; validate via the config-cache report.

for a principal

Plan a multi-module migration: conventions, lint rules against project.property, comms about increased (correct) invalidation, and verification gates.

## Why the violations appear The configuration cache serializes the configured task graph so future builds skip configuration. To make that sound, a cached task must not, at execution time, reach back into `Project`, `Gradle`, `Settings`, or live JVM globals like `System.getProperty`/`System.getenv`. Legacy property reads do exactly that: ```kotlin val flag = project.property("flag") // Project access val home = System.getenv("HOME") // live global at exec time ``` Gradle's config-cache report flags these as problems. ## The provider-based fix Swap each source to its provider equivalent and wire it lazily: ```kotlin abstract class DeployTask : DefaultTask() { @get:Input abstract val flag: Property<String> @get:Input abstract val token: Property<String> @TaskAction fun run() { /* use flag.get(), token.get() */ } } tasks.register<DeployTask>("deploy") { flag.set(providers.gradleProperty("flag").orElse("off")) token.set(providers.environmentVariable("DEPLOY_TOKEN").orElse("")) } ``` Mapping table: | Legacy | Provider | |---|---| | `project.property("x")` | `providers.gradleProperty("x")` | | `project.findProperty("x")` | `providers.gradleProperty("x")` (absent instead of null) | | `System.getProperty("x")` | `providers.systemProperty("x")` | | `System.getenv("X")` | `providers.environmentVariable("X")` | ## Subtle behavior changes to watch ### 1. Timing shift: configuration → execution Legacy reads happened during configuration; wired providers resolve at execution time. Code that depended on the *configuration-time* value (e.g., to decide which tasks to register) needs explicit handling — register decisions can still read eagerly via `getOrElse`, but be deliberate about it. ### 2. Absence semantics change `findProperty` returned `null`; `property` *threw*. The provider is simply *absent*. Old `if (project.findProperty("x") != null)` checks become `providers.gradleProperty("x").isPresent` (or `getOrNull() != null`), and any code that relied on the throw must add explicit error handling. Forgetting `.orElse` where a default was previously implicit is a common migration bug. ### 3. Inputs become tracked — invalidation changes Once wired, the property is a real cache input. Changing it now invalidates the configuration cache and re-runs affected tasks. Teams used to Gradle silently reusing stale values may see *more* re-runs — which is correct, but worth communicating. ### 4. Don't reintroduce eagerness Resist `providers.gradleProperty("x").get()` inside configuration blocks; that satisfies the type system but reintroduces eager, untracked reads and can throw on absence. Keep the value in provider form until execution. ## Validating the migration Run with `--configuration-cache` and inspect the HTML report until property-read problems are gone, then confirm a second build reports a cache hit. Test both the present and absent cases for each property so the new `.orElse` defaults behave as the old code did.

  • Old code used project.findProperty("x") != null. What's the provider equivalent?
    providers.gradleProperty("x").isPresent (a Provider<Boolean>) or getOrNull() != null. Don't forget to preserve any default the old null-check implied via .orElse.
  • Why might the team suddenly see more task re-runs after the migration?
    Wired properties are now tracked cache inputs, so changing one correctly invalidates the configuration cache and re-runs dependent tasks — behavior the eager reads previously masked.
  • How do you verify the property reads no longer violate the config cache?
    Build with --configuration-cache, read the generated HTML report until 'reading project property' / system-property problems are gone, then confirm a second run reports a configuration-cache hit.

saying these in an interview costs you the question

  • Swapping to providers but then calling .get() in configuration blocks, reintroducing the violation in spirit and risking absence crashes.
  • Forgetting that findProperty returned null while property threw, and dropping the implicit default during migration.
  • Assuming the migration is purely mechanical with no timing/absence semantic changes.

context