With a project dependency project(':lib'), how do api vs implementation affect what flows into :app's compile classpath and the induced graph?
answer
- java-library: apiElements vs runtimeElements
- api leaks to consumer compileClasspath
- implementation = runtime-only to consumers
- variant selection by Usage attribute
- implementation shrinks recompile fan-out
basics
~20 sIn :lib, deps declared with api leak onto :app's compile classpath transitively; implementation deps stay internal to :lib and only appear on :app's runtime classpath. This (from the java-library plugin) shapes what :app can see at compile time.
solid answer
~40 sThe `java-library` plugin splits :lib's outgoing artifacts into two consumable configurations: `apiElements` (compile-facing) and `runtimeElements` (runtime-facing). A dependency :lib declares with `api` is exposed via `apiElements`, so it lands on a consumer's `compileClasspath`; one declared with `implementation` goes only to `runtimeElements`, so it reaches the consumer's `runtimeClasspath` but **not** its compile classpath. When :app does `implementation(project(':lib'))`, :app sees :lib's *api* transitives at compile time and the full runtime closure at runtime. This affects both visibility *and* the task graph: :app:compileJava resolves apiElements; :app's run/test resolves runtimeElements. Using `implementation` aggressively in :lib shrinks the compile classpath, reducing recompilation fan-out when an internal dep of :lib changes — better incrementality across the graph.
code
kotlin · 10 lines// lib/build.gradle.kts
plugins { `java-library` }
dependencies {
api("com.google.guava:guava:33.0.0-jre") // visible to :app at compile time
implementation("org.apache.commons:commons-lang3:3.14.0") // hidden from :app compile
}
// app/build.gradle.kts
dependencies { implementation(project(":lib")) }
// :app compile cp -> :lib + guava ; :app runtime cp -> :lib + guava + commons-lang3go deeper
Know that api leaks a :lib dependency to :app's compile classpath while implementation keeps it internal/runtime-only.
Explain apiElements vs runtimeElements, attribute-based variant selection, and the compile/runtime classpath split for the consumer.
Tie api/implementation to encapsulation and incremental-build fan-out; reason about when promotion to api is justified by public signatures.
Set org conventions favouring implementation, treat api surface as a governed contract, and quantify recompilation cost across a large module graph.
## Two outgoing variants from a library Apply the `java-library` plugin to :lib and it publishes two **consumable** configurations: - `apiElements` — the **compile** view consumers get. - `runtimeElements` — the **runtime** view consumers get. (The plain `java` plugin has no `api` configuration — only `implementation` — so everything is runtime-only to consumers.) ## api vs implementation inside :lib ```kotlin // lib/build.gradle.kts plugins { `java-library` } dependencies { api("com.google.guava:guava:33.0.0-jre") // leaks to consumers' compile cp implementation("org.apache.commons:commons-lang3:3.14.0") // internal only } ``` - `api` deps are added to `apiElements` → a consumer that depends on :lib gets guava on its **compileClasspath** (and runtime). - `implementation` deps go only to `runtimeElements` → commons-lang3 is on the consumer's **runtimeClasspath** but invisible at compile time. ## The consumer side ```kotlin // app/build.gradle.kts dependencies { implementation(project(":lib")) } ``` - `:app:compileJava` resolves `compileClasspath`, selecting :lib's `apiElements` variant via attribute matching → sees :lib + guava, **not** commons-lang3. - `:app`'s runtime (run/test) resolves `runtimeClasspath` → :lib's `runtimeElements` → full closure including commons-lang3. Variant selection is driven by **attributes** (e.g. `org.gradle.usage = java-api` vs `java-runtime`). Gradle picks the variant whose attributes match what the consuming configuration requests. ## Why it matters for the graph and incrementality A smaller compile classpath means fewer modules participate in compile-time recompilation. If :lib's *internal* (`implementation`) dependency changes, consumers don't recompile against it — only :lib does. Leaking everything via `api` enlarges every consumer's compile classpath and recompilation fan-out. So `api`/`implementation` is both an **encapsulation** decision and a **build-performance** lever across the project graph. ## Common mistake Using `api` by default "to be safe" — it couples consumers to :lib's internals and slows incremental builds. Prefer `implementation`; promote to `api` only when a type genuinely appears in :lib's public API signatures.
- If you change an implementation dependency inside :lib, do consumers recompile?No, consumers don't recompile against it — it's not on their compile classpath. Only :lib recompiles (and consumers may relink at runtime). That's the incremental-build win of implementation over api.
- What plugin do you need for api to mean anything?java-library. The plain java plugin has no api configuration, so all deps are runtime-only to consumers and you lose the compile/runtime split.
- How does Gradle pick apiElements vs runtimeElements?Via attribute matching: the consumer's compileClasspath requests Usage=java-api, runtimeClasspath requests Usage=java-runtime, and Gradle selects the producer variant whose attributes match.
saying these in an interview costs you the question
- Saying api and implementation behave identically across projects.
- Defaulting everything to api 'to be safe' — it bloats consumer compile classpaths and recompilation.
- Forgetting the java-library plugin is required for api to exist.