skip to content

When wiring a consumer task's input from a producer, when do you use map versus flatMap, and how does each preserve the inferred dependency?

level: seniorimportance: should knowfreq 40%

answer

  1. map → plain value; flatMap → Provider
  2. TaskProvider IS a Provider<Task>
  3. flatMap avoids Provider<Provider<>>
  4. .get() at config time = drop producer link
  5. lazy operators carry build dependencies

basics

~20 s

Use map when the transform returns a plain value, flatMap when it returns another Provider. Both keep the producer task attached, so the inferred dependency survives. Avoid .get(), which resolves eagerly and drops the producer link.

solid answer

~40 s

`Provider.map { }` transforms the provided value lazily and returns a `Provider<R>`; use it when your lambda returns a *plain* value (e.g. read a task property's already-resolved field). `flatMap { }` is for when the lambda itself returns a `Provider` — typically `producer.flatMap { it.outputFile }`, where `it` is the task and `it.outputFile` is a `RegularFileProperty` (a provider). flatMap flattens the nested provider so you don't end up with `Provider<Provider<…>>`. Crucially, both operators preserve the *task-dependency metadata*: the resulting provider still knows the producer task, so Gradle's inference still fires. What breaks inference is calling `.get()` (or `.orNull`) during configuration — that eagerly extracts a value and discards the producer link. Rule of thumb: lambda returns a Provider → flatMap; returns a value → map; never `.get()` while wiring.

code

kotlin · 10 lines
kotlin
val produce = tasks.register<Produce>("produce") {
    out.set(layout.buildDirectory.file("gen/x.txt"))
}
tasks.register<Consume>("consume") {
    // flatMap: lambda returns RegularFileProperty (a Provider) -> dependency inferred
    src.set(produce.flatMap { it.out })

    // map: lambda returns a plain String derived lazily -> still carries producer
    label.set(produce.map { it.name.uppercase() })
}

go deeper

for a junior

Recognize producer.flatMap { it.output } as the standard wiring; don't worry about the map/flatMap nuance yet.

for a middle

State the rule: flatMap when the lambda returns a Provider, map when it returns a value; both stay lazy.

for a senior

Explain how lazy operators forward build-dependency metadata so inference survives chains, and how .get() breaks both inference and configuration avoidance.

for a principal

Codify provider-only wiring (ban config-time .get()) as a team convention to keep the graph correct and avoidance intact at scale.

## The two operators Gradle's `Provider<T>` is a lazy box. Two lazy transforms keep things lazy: - `map { t -> r }` → `Provider<R>`. Use when the lambda returns a **plain value**. - `flatMap { t -> providerOfR }` → `Provider<R>`. Use when the lambda returns a **Provider** (avoids the nested `Provider<Provider<R>>`). ## The canonical wiring shape A `TaskProvider<MyTask>` is itself a `Provider<MyTask>`. So: ```kotlin val producer: TaskProvider<Produce> = tasks.register<Produce>("produce") { ... } // producer is Provider<Produce>; it.out is RegularFileProperty (a Provider<RegularFile>) consumer.input.set(producer.flatMap { it.out }) // flatMap: lambda returns a Provider ``` If instead `it.out` were already a resolved value (not a provider) you'd use `map`. Because the producer is a real output property, you almost always reach for **flatMap** when wiring task outputs. ## Why the dependency survives Lazy operators carry **build dependencies** forward. The provider returned by `flatMap`/`map` remembers that resolving it requires the producer task to have run. When Gradle scans the consumer's annotated inputs, it asks each provider for its producer and finds the producer task — so the edge is inferred even through arbitrary `map`/`flatMap` chains. ## What breaks it ```kotlin // BAD: eager resolution at configuration time consumer.input.set(producer.get().out.get()) ``` `.get()` forces the value *now*, returning a plain `RegularFile`. That value carries no producer metadata, so: - inference fails (no edge added), and - you've also forced the producer task to be configured eagerly, defeating configuration avoidance. `orElse`, `zip`, and friends also preserve dependencies as long as you stay in provider-land and never call `.get()`/`.orNull` while configuring. ## Quick decision table | Lambda returns | Operator | |---|---| | a plain value | `map` | | another Provider | `flatMap` | | (configuring) needs the raw value now | nothing — you're doing it wrong; stay lazy | ## Why interviewers care Mixing these up produces either a compile error (`Provider<Provider<…>>`), or silent loss of the inferred dependency leading to flaky 'file not found' / stale-output bugs that only appear when the producer hasn't run yet.

  • Why not just use map everywhere?
    If the lambda returns a Provider, map gives you Provider<Provider<R>>, which won't type-check against a Property<R>. flatMap flattens it. Use map only when the lambda yields a plain value.
  • Do map/flatMap chains lose the inferred dependency?
    No. Each lazy operator forwards the build-dependency metadata, so even a long map/flatMap chain still resolves the producer task. Only eager extraction (.get()/.orNull at config time) drops it.
  • Is TaskProvider a Provider?
    Yes — TaskProvider<T> extends Provider<T>, which is exactly why producer.flatMap { it.output } works and carries the producer dependency.

saying these in an interview costs you the question

  • Saying flatMap and map are interchangeable.
  • Calling .get() inside the wiring to 'simplify' it — silently kills inference and eager-configures.
  • Believing a transform chain forgets the producer task.

context