skip to content

Consuming Specific Configurations Across Projects

Targeting a named producer configuration across projects, such as consuming another project's test fixtures. Interviewers ask because it reveals whether you think of a project as one artifact or as several publishable variants.

on this pageshow

questions

5

How do you depend on a *specific* named configuration of another project in the same build, rather than its default API/runtime, and what does the syntax look like?

level: middleimportance: must knowfreq 45%

answer

  1. project(path:, configuration:) two-arg form
  2. bypasses attribute matching
  3. producer config must be canBeConsumed
  4. Kotlin uses named args path =, configuration =
  5. testFixtures() is the attribute-driven shortcut

basics

~10 s

Use the two-arg project notation: project(path: ':lib', configuration: 'someConfig'). The configuration name selects which producer configuration to consume instead of the default one.

solid answer

~40 s

A plain `project(':lib')` dependency resolves against the producer's default outgoing variant (its API/runtime, chosen by attribute matching). To pull a *named* producer configuration instead, you use the explicit notation `project(path: ':lib', configuration: 'someConfig')` (Groovy) or `project(path = ":lib", configuration = "someConfig")` (Kotlin). This bypasses attribute-based variant selection and binds directly to the artifacts and dependencies that the named configuration exposes. It's the classic way to consume a non-default output of a sibling project — for example a `testFixtures`-style configuration, packaged client artifacts, or generated resources. Modern Gradle prefers attribute-driven selection (a single `project(':lib')` with attributes deciding the variant), but the explicit `configuration:` form is still valid and sometimes the simplest way to grab a specific output.

code

kotlin · 7 lines
kotlin
dependencies {
    // default variant (attribute-driven)
    implementation(project(":lib"))

    // a specific named producer configuration
    implementation(project(path = ":lib", configuration = "instrumentedJars"))
}

go deeper

for a junior

Recall that there is a two-arg form project(path:, configuration:) that picks a named configuration instead of the default.

for a middle

Explain the syntax in both Groovy and Kotlin and that it bypasses attribute matching, and note the producer config must be consumable.

for a senior

Contrast explicit configuration targeting with attribute-driven variant selection and explain when each is appropriate; mention testFixtures() as the higher-level helper.

for a principal

Frame a policy: prefer attribute/capability-driven consumption for maintainability, reserve explicit configuration targeting for narrow interop cases, and discuss migration paths off brittle explicit wiring.

## The problem When you write a project dependency in a multi-project build: ```kotlin dependencies { implementation(project(":lib")) } ``` Gradle has to decide *which* set of artifacts and transitive dependencies from `:lib` you actually get. By default it performs **variant-aware resolution**: it looks at the consumer's request attributes (e.g. `org.gradle.usage = java-api` vs `java-runtime`) and matches them against the producer's *consumable* configurations to pick exactly one variant. You never name a configuration — the attributes do the work. ## Targeting a specific configuration Sometimes you want to bypass that and grab a *named* output explicitly. The two-argument project notation does this: ```kotlin dependencies { implementation(project(path = ":lib", configuration = "instrumentedJars")) } ``` This says: "resolve `:lib`, but take exactly the configuration named `instrumentedJars` — its artifacts and its dependencies." Attribute matching is **not** used to choose the variant in this case; you've named it directly. ## What the producer must do For this to work, `:lib` must expose a configuration that is **consumable** (`canBeConsumed = true`) and carries the artifacts you want: ```kotlin // in :lib val instrumentedJars by configurations.creating { isCanBeConsumed = true isCanBeResolved = false } artifacts { add("instrumentedJars", instrumentJarTask) } ``` A configuration that is only a *bucket* (both `canBeConsumed` and `canBeResolved` false) or only *resolvable* cannot be consumed this way. ## When to use it vs attributes - **Explicit `configuration:`** — quick, direct, no attribute plumbing. Downside: brittle, ignores variant rules, and won't compose with attribute-based selection elsewhere. - **Attribute-driven** (plain `project(":lib")` + matching attributes) — the modern, recommended path; it's what plugins like `java-test-fixtures` build on top of via the `testFixtures(...)` helper. ## Test fixtures shortcut The `java-test-fixtures` plugin generates a `testFixtures` capability and gives you a helper: ```kotlin dependencies { testImplementation(testFixtures(project(":lib"))) } ``` That's attribute/capability driven under the hood — you rarely need the raw `configuration = "testFixtures..."` form once the plugin is applied.

  • What property must the producer configuration have for project(configuration:) to resolve against it?
    It must be consumable — isCanBeConsumed = true. A pure bucket or a resolvable-only configuration cannot be targeted this way.
  • What is the Kotlin DSL equivalent of the Groovy project(path: ':lib', configuration: 'x')?
    project(path = ":lib", configuration = "x") — Kotlin uses named arguments with = instead of Groovy's map-literal colon syntax.

saying these in an interview costs you the question

  • Saying project(':lib') always gives you 'all' the lib's outputs — it gives one attribute-selected variant.
  • Claiming you can target any configuration regardless of its canBeConsumed flag.

context

open as a page

If you write project(path: ':lib', configuration: 'reports') and resolution fails, what is likely wrong on the producer side and how do you make a configuration targetable this way?

level: middleimportance: should knowfreq 30%

basics

~10 s

The named configuration on :lib probably isn't consumable. Mark it isCanBeConsumed = true (and usually isCanBeResolved = false) and attach artifacts to it, then it can be targeted.

open as a page

Compare consuming a sibling project via an explicit project(configuration:) versus relying on attribute-driven variant selection. When would you deliberately choose the explicit form, and what are its downsides?

level: seniorimportance: should knowfreq 28%

basics

~10 s

Explicit project(configuration:) names exactly one output and skips attribute matching — direct but brittle. Attribute-driven selection (plain project(':lib')) is the modern, composable default that adapts to context like api vs runtime.

open as a page

In the absence of attribute-aware metadata, what is the 'default' configuration of a project, and how does it relate to project(':lib') versus project(path: ':lib', configuration: 'default')?

level: middleimportance: nice to knowfreq 15%

basics

~10 s

Legacy project dependencies resolve to a configuration literally named 'default'. project(':lib') is shorthand for project(path: ':lib', configuration: 'default') only when no variant-aware metadata drives selection.

open as a page

When you consume project(path: ':lib', configuration: 'foo'), what exactly do you receive — just artifacts, or dependencies too — and how does transitivity behave?

level: seniorimportance: nice to knowfreq 18%

basics

~10 s

You receive both the artifacts attached to 'foo' and the dependencies declared on 'foo', including their transitive closure. The named configuration's whole outgoing surface comes along, not just files.

open as a page