How do you read a value from gradle.properties lazily in a modern Gradle build, and why prefer providers.gradleProperty('x') over project.property('x')?
answer
- Provider<String>, lazy box
- no Project reference captured
- config-cache-safe alternative to project.property
- gradleProperty vs systemProperty vs environmentVariable
- wire, don't .get() at config time
basics
~10 sUse providers.gradleProperty("x"), which returns a Provider<String> read lazily at execution time. project.property("x") reads eagerly at configuration time and touches the Project object, which the configuration cache disallows.
solid answer
~40 s`providers.gradleProperty("x")` returns a `Provider<String>` that is **lazy** — the value is read when the provider is queried (`.get()`), not when the line runs. You wire that provider into a task input or another property and the actual lookup happens later. By contrast `project.property("x")` (and `project.findProperty`) is **eager**: it returns the resolved value immediately and requires a live `Project` reference. The configuration cache serializes the task graph and forbids holding a `Project` at execution time, so `project.property` breaks it; the provider, holding only the property name, survives. `gradleProperty` resolves only against project properties (gradle.properties, `-P`, `ORG_GRADLE_PROJECT_*`) — it does NOT read system properties or env vars, for which you use `providers.systemProperty` / `providers.environmentVariable`.
code
kotlin · 10 lines// Lazy, config-cache-safe
val flag: Provider<String> = providers.gradleProperty("myFlag").orElse("default")
tasks.register<JavaExec>("run") {
// wired lazily; read at execution time
args(flag.get()) // .get() inside execution-time closures is fine
}
// Eager, breaks the configuration cache:
// val v = project.property("myFlag")go deeper
Know that gradleProperty returns a lazy Provider<String> and is the config-cache-safe replacement for project.property.
Explain why project.property breaks the configuration cache (captures Project / eager read) and how wiring the provider into a task input fixes it.
Discuss composing providers (orElse/map), the three providers.* sources, and avoiding premature .get() at configuration time.
Frame a team convention: ban project.property in build logic, standardize provider wiring, and explain the impact on incremental + cached builds across many modules.
## The problem: eager vs lazy property reads Classic Gradle code reads a property like this: ```kotlin val v = project.property("myFlag") // eager, needs Project, runs at configuration time ``` This resolves *immediately* when the line executes (configuration time) and needs a live `Project` object. Two things make that bad in modern Gradle: 1. **Configuration cache**: Gradle serializes the configured task graph so subsequent builds skip configuration entirely. A serialized task must not hold a reference to `Project`, `Gradle`, `Settings`, etc. `project.property` forces exactly such a reference, so the cache reports it as a problem. 2. **Lazy configuration / wiring**: you often want to *describe* where a value comes from and defer reading it until it is actually needed (task execution), so values set later still take effect. ## The provider API `providers` is a `ProviderFactory`, available on `Project` (and injectable). Its property-reading methods each return a `Provider<String>`: - `providers.gradleProperty("x")` — a **project (Gradle) property**: from `gradle.properties`, `-Px=...`, or `ORG_GRADLE_PROJECT_x` env var. Returns empty (absent) if undefined. - `providers.systemProperty("x")` — a **JVM system property** (`-Dx=...`). - `providers.environmentVariable("X")` — an **environment variable**. A `Provider<T>` is a lazy box: it knows *how* to produce a value but only does so when queried via `.get()`, `.getOrNull()`, `.getOrElse(default)`, or `.orElse(...)`. Wiring it into a task input means Gradle reads it at execution time and tracks it as a build-cache/config-cache input automatically. ## Why this is configuration-cache-safe The provider captures only the *name* of the property and a reference to the provider factory machinery — not a `Project`. When Gradle serializes the task graph it can store "read gradle property x" as an input. On a cache hit it can detect that the property's value changed and invalidate correctly. No `Project` is captured, so no config-cache violation. ## Wiring, not getting The idiomatic move is to **wire** the provider, never `.get()` it at configuration time: ```kotlin tasks.register<MyTask>("run") { flag.set(providers.gradleProperty("myFlag").orElse("default")) } ``` Here `flag` is a `Property<String>` task input; the provider chain (`gradleProperty(...).orElse(...)`) is evaluated lazily and recorded as an input. Calling `.get()` in the configuration body would re-introduce eager reads and lose the laziness benefit. ## Defaults and transforms Providers compose: `.orElse("x")` supplies a fallback, `.map { it.toInt() }` transforms the value lazily, `.isPresent` checks existence without forcing a value at config time (when wired). This lets you express a whole pipeline that only runs on demand.
- What does providers.gradleProperty return when the property is undefined?A provider with no value (absent / not present). Querying .get() throws; use .getOrNull(), .getOrElse(default), or .orElse(...) to handle the missing case.
- Does gradleProperty read system properties set with -D?No. gradleProperty reads only project (Gradle) properties. For -D system properties use providers.systemProperty; for environment variables use providers.environmentVariable.
project.property is calling the warehouse now and reading you the number off the shelf; providers.gradleProperty hands you a slip that says 'go ask shelf x when you actually need it' — so the lookup happens later and you never carry the whole warehouse around.
saying these in an interview costs you the question
- Saying providers.gradleProperty reads -D system properties or env vars — it reads only project properties.
- Claiming it returns the value directly — it returns a Provider<String>, not a String.