skip to content

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%

answer

  1. attribute-driven = default, composable, context-sensitive
  2. explicit = one named config, no matching
  3. explicit is brittle: name coupling
  4. no api/runtime adaptation, bypasses capabilities
  5. doesn't translate to published metadata

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.

solid answer

~50 s

Attribute-driven selection is Gradle's default and preferred model: a plain `project(':lib')` dependency, and the consumer's attributes (Usage, Category, capabilities, etc.) pick exactly one producer variant. It's context-sensitive — the same dependency yields the API variant on the compile classpath and the runtime variant on the runtime classpath — and it composes with capability conflict resolution and feature variants. The explicit `project(path: ':lib', configuration: 'x')` form names a single configuration outright, ignoring attributes. You'd choose it for narrow, deliberate interop: grabbing one non-standard output (a custom zip, instrumented jars, a reports bundle) where modeling a full variant is overkill, or when bridging to a legacy producer that never declared attributes. Downsides: it's brittle (rename the config and consumers break), it ignores the variant rules so it can't adapt to api/runtime context, it bypasses capability/conflict handling, and it doesn't survive publication cleanly — published metadata is variant/attribute based. As a rule, prefer attributes; reach for explicit naming only as a tactical shortcut.

code

kotlin · 10 lines
kotlin
// Preferred: attributes decide api vs runtime per context
dependencies { implementation(project(":lib")) }

// Tactical: one fixed non-standard output, no adaptation
dependencies {
    implementation(project(path = ":lib", configuration = "docsBundle"))
}

// Best for a reusable special output: model it as a capability
dependencies { testImplementation(testFixtures(project(":lib"))) }

go deeper

for a junior

Know both forms exist and that the plain project(':lib') form is the usual one.

for a middle

Explain that explicit naming skips attribute matching and is more brittle than the default.

for a senior

Trade off context-sensitivity, capability handling, and publication implications; cite java-test-fixtures as the capability-based alternative.

for a principal

Establish org-wide guidance: model reusable special outputs as variants/capabilities, restrict explicit configuration consumption to interop, and consider impact on published metadata and consumer build maintainability.

## Two ways to consume a sibling ### 1. Attribute-driven (preferred) ```kotlin dependencies { implementation(project(":lib")) } ``` Gradle resolves `:lib` by **variant-aware matching**: the consuming configuration (e.g. `compileClasspath`) carries request attributes such as `org.gradle.usage=java-api`, and Gradle finds the producer's consumable variant whose attributes best match. The `java-library` plugin models this for you with `apiElements` (api usage) and `runtimeElements` (runtime usage). Result: one dependency declaration, correct variant per context, automatic transitivity, and capability-aware conflict resolution. ### 2. Explicit configuration targeting ```kotlin dependencies { implementation(project(path = ":lib", configuration = "instrumentedJars")) } ``` This names one producer configuration directly. **No attribute matching** happens for the selection — you get exactly that configuration's artifacts and dependencies, regardless of whether you're on a compile or runtime classpath. ## When explicit is justified - A genuinely **non-standard output** that doesn't map to a Java usage (e.g. a documentation bundle, a generated-resources zip, instrumented test jars). - **Legacy / interop** with a producer that predates attribute modeling. - A quick **internal-only** wiring where the overhead of declaring attributes isn't worth it. ## Downsides of going explicit - **Brittle coupling**: consumers hard-code the configuration *name*; renaming or restructuring the producer breaks them silently. - **No context sensitivity**: it can't give api on compile and runtime on run — it's one fixed set. - **Bypasses capabilities/conflicts**: capability conflict resolution and feature-variant logic don't apply. - **Publication mismatch**: if `:lib` is ever published, consumers downstream resolve via Gradle Module Metadata attributes, not raw configuration names — so the explicit wiring doesn't translate. ## Decision heuristic Default to attribute-driven. If you find yourself reaching for `configuration:`, ask whether the output deserves a proper variant (attributes + capability) instead — especially if more than one consumer needs it or the library is published. The `java-test-fixtures` plugin is the canonical example of converting "a special configuration" into a proper capability you consume with `testFixtures(project(...))`.

  • Why does explicit configuration targeting not survive publication well?
    Published artifacts expose Gradle Module Metadata describing variants by attributes/capabilities, not by raw configuration names. External consumers resolve by attribute matching, so a name-based dependency has nothing to bind to.
  • Give an example where attribute-driven selection adapts but explicit cannot.
    With attributes, project(':lib') yields the API variant on compileClasspath and the runtime variant on runtimeClasspath automatically. An explicit configuration name returns the same fixed set regardless of which classpath consumes it.

Attribute-driven selection is ordering 'a coffee for the morning' and trusting the barista to pick the right roast for the time of day; explicit configuration is pointing at one exact urn — you get that urn no matter the context, and if they move it you're stuck.

saying these in an interview costs you the question

  • Claiming explicit configuration targeting is the recommended modern approach.
  • Saying it composes with capability conflict resolution — it does not, because it bypasses variant selection.

context