Walk through how api vs implementation on project dependencies controls what a downstream consumer can see across a chain of subprojects (:app → :service → :core).
answer
- compile transitivity only via api
- runtime transitivity always (api + implementation)
- apiElements vs runtimeElements outgoing variants
- implementation firewalls recompilation
- declare direct dep instead of relying on leak
basics
~20 sCompile-time visibility flows only through api edges. If :service declares api(project(":core")), then :app (depending on :service) can compile against :core. If it's implementation, :core is hidden from :app at compile time but still present at runtime.
solid answer
~40 sConsider `:app → :service → :core`. Compile-time **transitivity is carried only by `api`**: - `:service` declares `api(project(":core"))` → `:core` is on `:service`'s `apiElements`, so `:app` gets `:core` on its **compile** classpath and can reference its types. - `:service` declares `implementation(project(":core"))` → `:core` is on `:service`'s `runtimeElements` only. `:app` gets `:core` at **runtime** but **not** at compile time; `:app` source cannot name `:core`'s types. Runtime is transitive regardless: both `api` and `implementation` deps end up on the consumer's runtime classpath, because you need them to actually execute. The practical upshot: keep cross-project deps as `implementation` to firewall compile-time coupling, and promote to `api` only the modules whose types you deliberately re-export. This minimises the recompilation graph and prevents `:app` from secretly compiling against transitive internals.
code
kotlin · 12 lines// service/build.gradle.kts
plugins { `java-library` }
dependencies {
// EXPOSE :core to :service's consumers (compile-transitive)
api(project(":core"))
// vs. HIDE it (runtime-transitive only):
// implementation(project(":core"))
}
// app/build.gradle.kts — with api above, :app can import :core types;
// with implementation above, :app code referencing :core fails to compile
// (but :core is still on :app's RUNTIME classpath).go deeper
Not expected to trace multi-hop chains; basic api-leaks/implementation-hides is enough.
Should distinguish compile vs runtime transitivity across one hop.
Trace the full :app → :service → :core chain, name apiElements/runtimeElements, and connect to recompilation blast radius.
Use these rules to design module dependency policies, detect leakage via tooling, and decide where API boundaries should sit across a large module graph.
## The chain ``` :app --(depends on)--> :service --(depends on)--> :core ``` The question is: when `:app` depends on `:service`, what of `:core` does `:app` see, and at which phase (compile vs runtime)? ## Two outgoing variants per module The `java-library` plugin gives every module two consumable (outgoing) configurations: - **`apiElements`** — consumed at the **compile** classpath of downstream modules. Contains the module's own API artifacts **plus** its `api` dependencies. - **`runtimeElements`** — consumed at the **runtime** classpath of downstream modules. Contains the module's artifacts **plus** both its `api` *and* `implementation` dependencies. Gradle uses **attribute matching** (e.g. `org.gradle.usage = java-api` vs `java-runtime`) to pick the right one for each consumer classpath. ## Tracing the cases ### Case 1 — `:service` uses `api(project(":core"))` - `:core` lands in `:service`'s `apiElements`. - `:app`'s compile classpath resolves `:service`'s `apiElements`, which transitively includes `:core`. - **Result:** `:app` can `import` and compile against `:core` types. Compile-time coupling propagates. ### Case 2 — `:service` uses `implementation(project(":core"))` - `:core` lands only in `:service`'s `runtimeElements`, not `apiElements`. - `:app`'s **compile** classpath sees `:service` but **not** `:core`. - `:app`'s **runtime** classpath resolves `:service`'s `runtimeElements`, which includes `:core`. - **Result:** `:app` cannot name `:core`'s types in source, but `:core` is present when the app runs. Compile-time coupling is firewalled. ## Why this is the senior insight 1. **Recompilation blast radius** — an ABI change in `:core` only forces recompilation of modules that can *see* it at compile time. `implementation` edges stop that propagation, so incremental builds are faster. 2. **Encapsulation** — `implementation` prevents `:app` from accidentally depending on `:service`'s internal choice of `:core`, so `:service` can swap `:core` for something else without breaking `:app`'s source. 3. **Leak detection** — if `:app` mysteriously compiles against `:core`, look for an unintended `api(project(":core"))` somewhere in the chain. ## Forcing visibility deliberately If `:app` genuinely needs `:core` at compile time, the clean fix is for `:app` to declare its **own** explicit dependency on `:core` (direct, intentional), rather than relying on a transitive `api` leak — making the dependency visible and reviewable.
- If :service uses implementation(project(":core")) but :app needs :core at compile time, what's the clean fix?Have :app declare its own direct dependency on :core (implementation or api as appropriate). This makes the coupling explicit and reviewable rather than relying on a transitive api leak through :service.
- Is :core ever on :app's runtime classpath when :service uses implementation?Yes. Runtime is transitive: :service's runtimeElements includes its implementation deps, so :core is present when :app runs — it's only hidden from :app's compile classpath.
- How does Gradle decide between apiElements and runtimeElements for a given classpath?Through attribute matching. The consumer's compile classpath requests the java-api usage attribute and runtime requests java-runtime; Gradle selects the matching outgoing variant of the producer.
saying these in an interview costs you the question
- Saying implementation deps are absent at runtime for downstream consumers — runtime is transitive.
- Claiming api and implementation differ only in 'style' with no behavioural effect.
- Recommending relying on transitive api leakage rather than declaring an explicit direct dependency.