skip to content

What are the three roles a Gradle configuration can play, and what does each one do?

level: middleimportance: must knowfreq 55%

answer

  1. bucket = declare
  2. resolvable = compute classpath
  3. consumable = expose variant
  4. isCanBeResolved / isCanBeConsumed
  5. extendsFrom wiring

basics

~10 s

A configuration can be a dependency-scope (bucket) that collects declared dependencies, resolvable (you ask it to compute a classpath), or consumable (exposed as a variant to other projects).

solid answer

~40 s

Gradle configurations have three orthogonal roles. A **dependency-scope** (a.k.a. bucket) is where you *declare* dependencies — e.g. `implementation`, `api`; it is neither resolved nor consumed directly. A **resolvable** configuration is one you ask Gradle to *resolve* into a concrete artifact set / classpath — e.g. `compileClasspath`, `runtimeClasspath`; it extends from buckets. A **consumable** configuration is *exposed to other projects* as a variant they can pick up — e.g. `apiElements`, `runtimeElements`. Each role is controlled by two boolean flags, `isCanBeResolved` and `isCanBeConsumed`. The clean split (declare → resolve → consume) avoids the legacy footgun where a single configuration tried to do everything, which produced confusing resolution and accidental leakage of dependencies into published variants.

code

kotlin · 8 lines
kotlin
// implementation = bucket (declare)
// compileClasspath = resolvable (consume for THIS build)
// apiElements = consumable (expose to other projects)
configurations {
    named("runtimeClasspath") {
        require(isCanBeResolved && !isCanBeConsumed)
    }
}

go deeper

for a junior

Name the three roles and give one example configuration for each (implementation, compileClasspath, apiElements).

for a middle

Map each role to the isCanBeResolved/isCanBeConsumed flags and explain the declare → resolve → consume flow with extendsFrom.

for a senior

Explain why the split exists (legacy do-everything footgun, variant leakage) and how it underpins variant-aware resolution.

for a principal

Frame role separation as the contract that lets you publish stable, curated variants across a large multi-project / platform without consumers depending on internal classpath shape.

## What a configuration is A **configuration** is a named set of dependencies plus the rules for how Gradle treats that set. Historically a configuration was a do-everything object, which caused subtle bugs. Modern Gradle gives every configuration one of three **roles**. ## The three roles - **Dependency-scope (bucket)** — a place to *declare* dependencies. `implementation`, `api`, `testImplementation` are buckets. You add `dependencies { implementation("...") }` here. A bucket is *not* resolved and *not* consumed directly; it just holds declarations that resolvable/consumable configurations inherit via `extendsFrom`. - **Resolvable** — something Gradle can *resolve*: turn the (transitive) declared dependencies into a concrete set of files/artifacts. `compileClasspath` and `runtimeClasspath` are resolvable. Resolving runs dependency resolution and variant selection. - **Consumable** — something *other projects* can depend on. These are the *variants* you publish. `apiElements` and `runtimeElements` are consumable; they carry attributes (e.g. `org.gradle.usage`) so consumers select the right one. ## The two flags Each role maps to two booleans on a `Configuration`: - `isCanBeResolved` — true only for resolvable. - `isCanBeConsumed` — true only for consumable. A bucket has **both false**. A configuration should have exactly one of them true; having both true is the deprecated legacy mode. ## extendsFrom wiring Buckets feed resolvable/consumable configurations through `extendsFrom`. For example `runtimeClasspath extendsFrom implementation, runtimeOnly`. You declare in buckets; the resolvable/consumable configs inherit those declarations. ```kotlin // Inspect roles configurations.named("implementation").get().let { require(!it.isCanBeResolved && !it.isCanBeConsumed) // bucket } configurations.named("runtimeClasspath").get().let { require(it.isCanBeResolved && !it.isCanBeConsumed) // resolvable } configurations.named("runtimeElements").get().let { require(!it.isCanBeResolved && it.isCanBeConsumed) // consumable } ``` ## Why it matters The role split is what makes variant-aware dependency management work cleanly: you declare once, resolve for your own build, and expose curated variants to consumers without the three concerns bleeding into each other.

  • Can a single configuration be both resolvable and consumable?
    Technically the legacy API allowed both flags true, but it is deprecated and discouraged. A configuration should serve exactly one role; combining them causes ambiguous resolution and accidental leakage of variants.
  • Which role does `implementation` have, and why can't you resolve it directly?
    `implementation` is a dependency-scope bucket: both flags false. It only holds declarations; `compileClasspath`/`runtimeClasspath` extend from it and are the resolvable configs that actually compute a classpath.

Think of a warehouse: the bucket is the receiving dock where stock is declared, the resolvable role is picking an order for your own use, and the consumable role is the shipping bay that hands curated pallets to other companies.

saying these in an interview costs you the question

  • Saying every configuration can be resolved (buckets like `implementation` cannot).
  • Confusing 'consumable' (exposed to others) with 'resolvable' (computed for me).

context