skip to content

What is the difference between Provider<T> and Property<T> in Gradle's lazy configuration API?

level: juniorimportance: must knowfreq 70%

answer

  1. Provider = read-only lazy value
  2. Property extends Provider + set()/convention()
  3. get / getOrNull / getOrElse / isPresent
  4. create via objects.property(Type::class)
  5. resolved at execution, not configuration

basics

~10 s

Provider<T> is a read-only lazy value you query with get(). Property<T> extends Provider<T> and is also writable — you can set() its value or wire it to another provider.

solid answer

~40 s

`Provider<T>` represents a value that is **calculated lazily** — it is read-only and you obtain the value via `get()`, `getOrNull()`, or `getOrElse()`. `Property<T>` is a sub-interface of `Provider<T>` that adds a **mutable** side: `set(value)` and `set(provider)`, plus `convention(...)` for defaults. So every `Property` is a `Provider`, but not vice versa. In a task or extension you typically declare inputs as `Property<T>` so users can configure them, and expose derived/computed values as `Provider<T>`. The key win over a plain field is **deferred resolution**: the value isn't computed at configuration time but when something actually reads it — usually during task execution — which lets Gradle build the value lazily and avoid eager work it may not need.

code

kotlin · 6 lines
kotlin
val objects = project.objects
val message: Property<String> = objects.property(String::class)
message.set("hello")           // Property is writable
val shout: Provider<String> =  // Provider is read-only / derived
    message.map { it.uppercase() }
println(shout.get())            // resolved lazily -> HELLO

go deeper

for a junior

State that Provider is read-only (get) and Property adds set(); creating via project.objects.

for a middle

Explain the inheritance (Property extends Provider), convention(), and why you declare inputs as Property and expose derived values as Provider.

for a senior

Tie it to deferred resolution at execution time, automatic task input/output wiring, and configuration-cache compatibility.

for a principal

Frame lazy types as the org-wide standard for plugin authoring, enabling config-cache adoption and avoiding eager configuration-phase cost across a large build.

## The problem lazy types solve In old Gradle code you'd write a task with a plain field like `String outputName` and assign it during the configuration phase. That forces the value to be computed eagerly, even if the task never runs, and it can't react to values produced later by other tasks. The **lazy configuration API** replaces those fields with `Provider<T>` and `Property<T>` so values are computed **on demand**. ## Provider<T> A `Provider<T>` is a **read-only** container for a value that will be calculated lazily. You never store the raw value in it directly — you query it: - `get()` — returns the value, or throws `IllegalStateException` if no value is present. - `getOrNull()` — returns the value or `null` if absent. - `getOrElse(default)` — returns the value or the supplied default. - `isPresent` — `true` if a value can be computed. ## Property<T> `Property<T>` **extends** `Provider<T>` and adds mutation: - `set(T)` / `set(Provider<T>)` — assign a value or wire to another provider. - `convention(T)` / `convention(Provider<T>)` — a fallback used only if nothing is explicitly set. - `value(...)` — set and return the property (handy in a fluent style). Because `Property` *is* a `Provider`, you can pass it anywhere a `Provider` is expected. This is the everyday split: declare configurable **inputs** as `Property` on your extension/task, and expose computed/derived outputs as `Provider`. ## Creating them You don't `new` these. You ask Gradle's `ObjectFactory` (`project.objects`): ```kotlin val name: Property<String> = objects.property(String::class) val upper: Provider<String> = name.map { it.uppercase() } ``` ## Deferred resolution The value behind a `Property` set with `set(otherProvider)` is **not** evaluated until someone calls `get()` — typically during the execution phase. That means a property can depend on a value produced by another task and still resolve correctly, because resolution happens after configuration is done. ## Why it matters Lazy types give you: deferred computation (no wasted work), correct **task input/output wiring** (Gradle can track dependencies automatically when one task's output `Provider` feeds another's input `Property`), and better support for the **configuration cache**, which requires that values be captured as providers rather than eagerly resolved Java fields.

  • Can you call set() on a Provider<T>?
    No. Provider<T> is read-only — it has no set(). Only Property<T> (which extends Provider) is mutable. To derive a new value from a Provider you use transformations like map/flatMap, which return another Provider.
  • Why prefer Property over a plain Kotlin var on a task?
    A Property defers resolution to execution time, can be wired to other tasks' outputs so Gradle tracks dependencies automatically, supports conventions, and is compatible with the configuration cache — a plain field is eager and breaks that wiring.

A Provider is like a read-only sealed envelope you can only open (get) when you need it; a Property is the same envelope but you're also allowed to put new contents in (set).

saying these in an interview costs you the question

  • Saying Provider is writable — it has no set().
  • Claiming Property and Provider are unrelated types rather than Property extending Provider.
  • Thinking the value is computed when you call set() rather than when get() reads it.

context