skip to content

Explain `extendsFrom` and walk through how a declaration in `api` ends up on a downstream consumer's compile classpath.

level: middleimportance: should knowfreq 30%

answer

  1. extendsFrom = inherit declarations
  2. api flows into apiElements + compileClasspath
  3. implementation -> runtimeElements only
  4. consumer compileClasspath selects apiElements
  5. implementation does not leak to compile

basics

~10 s

extendsFrom makes one configuration inherit the dependencies declared in another. api is included in both apiElements (exposed) and compileClasspath, so its dependencies propagate to consumers' compile classpaths.

solid answer

~40 s

`extendsFrom` builds an inheritance hierarchy among configurations: a configuration that `extendsFrom(other)` includes everything declared in `other`. Buckets are the leaves you declare into; resolvable and consumable configurations extend the relevant buckets to pull those declarations in. The `api` bucket is special: it is extended by the **consumable** `apiElements` (so its dependencies are published as part of the project's API variant) *and* by `compileClasspath`. So when project B depends on project A, B's `compileClasspath` resolves A's `apiElements`, which carries A's `api` dependencies transitively — they land on B's compile classpath. By contrast `implementation` is extended only by A's own `runtimeClasspath`/`runtimeElements`, not by `apiElements`, so it never leaks onto B's *compile* classpath — only its runtime. That difference is exactly what `api` vs `implementation` encodes, and `extendsFrom` is the wiring that enforces it.

code

groovy · 7 lines
groovy
// Inspect what flows where
// ./gradlew :A:dependencies --configuration apiElements
configurations {
    // compileClasspath extendsFrom api (via implementation) -> consumers see api deps
    // apiElements extendsFrom api -> exposed at compile time
    // runtimeElements extendsFrom implementation -> exposed at runtime only
}

go deeper

for a junior

State that extendsFrom makes one configuration inherit another's dependencies.

for a middle

Trace api → apiElements → consumer compileClasspath and contrast with implementation → runtime only.

for a senior

Connect the hierarchy to API/ABI compatibility and explain the consumer impact of api↔implementation moves.

for a principal

Use the hierarchy to reason about API governance and leakage across a large module graph and published artifacts.

## extendsFrom basics `configurations { a { extendsFrom(b) } }` means: configuration `a` includes all dependencies declared in `b` (transitively through the extends chain). It's pure inheritance of *declarations* — no resolution happens until a resolvable configuration is resolved. ## The standard Java hierarchy The Java Library plugin wires roughly: - Buckets: `api`, `implementation`, `compileOnly`, `runtimeOnly`. `implementation extendsFrom api`. - Resolvable: `compileClasspath extendsFrom implementation, compileOnly` (and thus `api`); `runtimeClasspath extendsFrom implementation, runtimeOnly` (and `api`). - Consumable: `apiElements extendsFrom api` (compile-time view exposed to consumers); `runtimeElements extendsFrom implementation, runtimeOnly` (runtime view). ## The propagation walk (B depends on A) 1. In A: `dependencies { api("com.example:lib:1.0") }` — declared in the `api` bucket. 2. A's `apiElements` (consumable) `extendsFrom api`, so `lib` is part of A's published *compile* API variant. 3. B declares `implementation(project(":A"))`. 4. B resolves `compileClasspath`. Variant selection picks A's `apiElements` for the compile usage. 5. Because `apiElements` carries `lib` transitively, `lib` appears on B's compile classpath. ```kotlin // Project A dependencies { api("com.google.guava:guava:33.0.0-jre") // leaks to consumers' compile classpath implementation("org.apache.commons:commons-lang3:3.14.0") // runtime only for consumers } ``` ## Why implementation does NOT leak to compile `implementation` is *not* extended by `apiElements`. It only feeds `runtimeElements`/`runtimeClasspath`. So a consumer compiling against A never sees A's `implementation` deps — they only show up at the consumer's runtime. That is the whole point of `api` vs `implementation`: `extendsFrom` decides which consumable variant each bucket flows into, and therefore what consumers see at compile vs runtime. ## Inspecting it ```bash ./gradlew :A:dependencies --configuration apiElements ./gradlew :B:dependencies --configuration compileClasspath ``` ## Takeaway `extendsFrom` is the inheritance mechanism; the role split decides *which* configurations extend which buckets, and together they implement the API/implementation visibility contract.

  • If you change a dependency from `api` to `implementation` in a library, what breaks for consumers?
    Consumers that referenced that transitive dependency at *compile* time stop seeing it (it's now runtime-only for them) and may fail to compile. It's a binary/source-compatibility change for the library's API surface.
  • Does `extendsFrom` trigger resolution?
    No. It only inherits declarations. Resolution happens lazily when a resolvable configuration (like compileClasspath) is actually resolved.

saying these in an interview costs you the question

  • Claiming `implementation` dependencies appear on a consumer's compile classpath.
  • Treating `extendsFrom` as a runtime concept rather than declaration inheritance.
  • Saying `extendsFrom` copies dependencies (it inherits a live view, not a snapshot).

context