When you declare implementation(project(":lib")), what exactly does :app consume from :lib, and how does Gradle choose it?
answer
- request not a file path
- outgoing variants apiElements/runtimeElements
- attribute matching (usage java-api/java-runtime)
- classes-dir avoids building the JAR
- outgoingVariants / dependencyInsight to debug
basics
~10 sBy default Gradle consumes :lib's main java component — its produced JAR (or classes/resources for compile) plus its exported dependencies — selecting the correct outgoing configuration via attribute matching, not a hardcoded JAR path.
solid answer
~40 sA bare `project(":lib")` dependency doesn't name a file — it asks for `:lib`'s **default output for this context**. Gradle resolves it through **variant-aware** dependency resolution: - The consumer classpath carries **attributes** (e.g. `org.gradle.usage = java-api` for compile, `java-runtime` for runtime, plus `TargetJvmVersion`, `LibraryElements`, etc.). - `:lib` exposes **outgoing variants** (`apiElements`, `runtimeElements`) each tagged with attributes. - Gradle picks the variant whose attributes are compatible with the request. For compile it may even supply `:lib`'s **classes directory** directly (avoiding building the JAR) when `LibraryElements = classes` matching is possible, speeding up the build. So "what is consumed" is the matched variant's artifacts plus its transitive (exported) dependencies — not a fixed `lib.jar`. This is why you don't (and shouldn't) reference output paths manually; the producer declares variants and the consumer's attributes drive selection.
code
bash · 6 lines# See the outgoing variants :lib exposes and their attributes
./gradlew :lib:outgoingVariants
# See exactly what :app resolved for :lib on the compile classpath
./gradlew :app:dependencyInsight \
--configuration compileClasspath --dependency :libgo deeper
Not expected; just know it gives you :lib's compiled output.
Should know compile vs runtime get different things and it's not a fixed JAR path.
Explain variant-aware resolution, attribute matching, and the classes-dir compile optimisation; know the diagnostic tasks.
Reason about custom attributes/variants for multi-target modules and how variant design affects build correctness and performance at scale.
## A project dependency is a *request*, not a file ```kotlin implementation(project(":lib")) ``` This says: "give me `:lib`'s appropriate output **for the configuration I'm resolving**". It does **not** mean `lib/build/libs/lib.jar`. The actual artifact is chosen at resolution time. ## Outgoing variants and attributes The `java`/`java-library` plugins publish `:lib` as a **component** with consumable (outgoing) configurations: - `apiElements` — attributes include `org.gradle.usage = java-api`. Used for downstream **compile** classpaths. - `runtimeElements` — attributes include `org.gradle.usage = java-runtime`. Used for downstream **runtime** classpaths. Each also carries attributes such as `org.gradle.jvm.version` (target JVM), `org.gradle.libraryelements` (`jar` vs `classes`/`resources`), and `org.gradle.category = library`. ## Attribute matching When `:app` resolves its `compileClasspath`, that configuration requests `java-api` usage. Gradle compares the request against `:lib`'s outgoing variants and selects `apiElements`. For `:app`'s `runtimeClasspath` it requests `java-runtime` and selects `runtimeElements`. This is **variant-aware resolution** — the same project dependency yields different artifacts in different contexts. ## The classes-dir optimisation For compilation, Gradle can match `LibraryElements = classes` and hand the consumer `:lib`'s **compiled classes directory** instead of a packaged JAR. That means `:app` can compile without `:lib`'s `jar` task running at all — only `:lib`'s `compileJava`/`classes` is needed. This shaves time off multi-module builds. (At runtime the JAR variant is typically used.) ## Transitive dependencies travel with the variant The selected variant also brings `:lib`'s exported dependencies (its `api` deps for the compile variant; `api` + `implementation` for the runtime variant), so the consumer gets the full, correct closure. ## Practical implications - **Don't** reference `project(":lib").buildDir` or hardcode JAR paths; let variant resolution work. - If `:lib` produces multiple variants (e.g. multiple JVM targets), the consumer's attributes (like `TargetJvmVersion`) disambiguate. - Resolution failures like "no variants ... match the consumer attributes" mean the producer doesn't expose a compatible variant — usually a missing plugin or attribute mismatch. ```kotlin // Inspect what variants :lib exposes: // ./gradlew :lib:outgoingVariants // Inspect what :app actually resolved: // ./gradlew :app:dependencyInsight --configuration compileClasspath --dependency :lib ```
- Why might compiling :app not trigger :lib's jar task?Because Gradle can match the LibraryElements=classes variant and hand :app the compiled classes directory directly, so only :lib's compileJava/classes runs — not jar. This is a deliberate multi-module build speedup.
- How do you debug a 'no matching variant' resolution error for a project dependency?Run :lib:outgoingVariants to see what the producer exposes and their attributes, and :app:dependencyInsight to see what the consumer requested. The mismatch is usually a missing plugin or an attribute like TargetJvmVersion that doesn't line up.
saying these in an interview costs you the question
- Saying project(":lib") always means the lib JAR at a fixed path.
- Hardcoding output file paths instead of relying on variant resolution.
- Believing compile and runtime classpaths get identical artifacts from the same project dependency.