Do inferred provider dependencies work across projects in a multi-module build, and how should they be exposed between modules?
answer
- provider carries producer across projects
- direct task reach-in breaks isolation
- consumable vs resolvable configurations
- outgoing artifact carries built-by
- resolve config → infers producer task
basics
~20 sYes — a provider from another project's task carries that task as producer, so consuming it infers a cross-project dependency. The clean way to expose outputs across modules is via variant-aware configurations (consumable/resolvable) rather than reaching into another project's tasks.
solid answer
~40 sInference works across projects: if project B consumes a `Provider` produced by a task in project A (e.g. `project(":a").tasks.named<Gen>("gen").flatMap { it.out }`), the provider still carries A's task as producer, so Gradle infers the cross-project edge. However, reaching into another project's tasks couples B to A's internals and fights project isolation / parallel configuration. The idiomatic alternative is **variant-aware dependency management**: project A declares a *consumable* configuration with `outgoing.artifact(genTask.flatMap { it.out })` (the artifact carries its producer), and B declares a *resolvable* configuration that `extendsFrom`/depends on `project(":a")` and resolves it. The artifact's built-by metadata flows through, so resolving B's configuration infers running A's task — inference, but decoupled and isolation-friendly. Prefer configurations over cross-project task references for anything beyond trivial builds.
code
kotlin · 11 lines// producer project :a
val gen = tasks.register<Gen>("gen") { out.set(layout.buildDirectory.file("a.txt")) }
val sharedOut = configurations.consumable("sharedOut")
artifacts { add(sharedOut.name, gen.flatMap { it.out }) }
// consumer project :b
val sharedIn = configurations.resolvable("sharedIn")
dependencies { add(sharedIn.name, project(":a")) }
tasks.register("useShared") {
inputs.files(sharedIn) // resolving infers :a:gen — no dependsOn
}go deeper
Know that consuming another project's output provider can make Gradle run that project's task too.
Explain that the provider carries its producer across projects, but reaching into another project's tasks is discouraged.
Describe consumable vs resolvable configurations and how an outgoing artifact's built-by metadata lets resolution infer the producer task cleanly across modules.
Set module-boundary conventions: expose outputs via variant-aware configurations, ban cross-project task reach-in, to preserve isolation, parallel configuration, and refactor safety at scale.
## Short answer: yes, but be careful A `Provider` doesn't care which project its producing task lives in. If project `:b` wires an input from a task in `:a`, the provider carries `:a`'s task as producer and Gradle infers the cross-project edge. So this *works*: ```kotlin // in :b (tightly coupled — avoid in real builds) val genA = project(":a").tasks.named<Gen>("gen") tasks.register("useA") { inputs.file(genA.flatMap { it.out }) } ``` ## Why the direct form is discouraged Reaching into `:a`'s task container: - **Breaks project isolation** — `:b` now depends on `:a`'s task names and types; renaming a task in `:a` breaks `:b`. - **Hurts parallelism** — forces `:a` to be configured when `:b` is configured. - **Doesn't scale** — couples module internals across the graph. ## The idiomatic alternative: variant-aware configurations Gradle models artifacts as **configurations** with two roles: - **consumable** (`canBeConsumed = true, canBeResolved = false`) — what a project *exposes*. - **resolvable** (`canBeResolved = true, canBeConsumed = false`) — what a project *requests*. Producer `:a` exposes its output: ```kotlin val shared = configurations.consumable("sharedOut") artifacts { add(shared.name, genTask.flatMap { it.out }) } // artifact carries built-by ``` Consumer `:b` resolves it: ```kotlin val incoming = configurations.resolvable("sharedIn") dependencies { add(incoming.name, project(":a")) } tasks.register("useShared") { inputs.files(incoming) } // resolving infers running :a's task ``` Because the published artifact carries its producer's built-by metadata, resolving `:b`'s configuration **infers** that `:a`'s task must run — the same inference principle, now across a clean module boundary with attribute matching. ## Decision guidance - Trivial/private internal builds: a direct cross-project provider may be fine. - Anything shared, reused, or isolation-sensitive: use consumable/resolvable configurations (or `project(path, configuration)` dependencies), so the dependency is declarative, attribute-matched, and cache/isolation friendly. ## Tie-back to the topic In all cases the *mechanism* is identical: a provider (or a configuration's artifact) carries its producing task, and consuming it infers the dependency without an explicit `dependsOn`. The design choice is *how* you let that provider cross the project boundary.
- What's the difference between a consumable and a resolvable configuration?Consumable (canBeConsumed=true, canBeResolved=false) is what a project exposes to others; resolvable (canBeResolved=true, canBeConsumed=false) is what a project requests/resolves. A configuration should be one or the other, not both (avoid legacy bucket configurations).
- Why prefer configurations over project(":a").tasks.named(...)?Configurations keep modules decoupled and isolation-friendly: B depends on A's published artifact and attributes, not A's task names/types. It scales, parallelizes, and survives refactors in A.
- Does resolving a configuration still rely on the same inference mechanism?Yes — the artifact added to the consumable configuration carries its producing task's built-by metadata, so resolving the consumer's configuration infers running that task, exactly like provider wiring.
saying these in an interview costs you the question
- Claiming inference can't cross project boundaries — it can.
- Defaulting to project(":a").tasks.named(...) in a real multi-module build, breaking isolation.
- Declaring a configuration as both consumable and resolvable.