When wiring one subproject onto another with project(...), what is the difference between declaring it with api(project(':lib')) versus implementation(project(':lib'))?
answer
- api = leaks to consumers; implementation = hidden
- api needs java-library plugin
- use api only for public signatures
- apiElements vs runtimeElements
- implementation = runtime-transitive, not compile-transitive
basics
~10 simplementation(project(":lib")) keeps :lib private to the consumer — downstream projects can't see it. api(project(":lib")) exposes :lib transitively to anyone depending on the consumer. api needs the java-library plugin.
solid answer
~40 sBoth put `:lib` on the consumer's compile and runtime classpath. The difference is **transitivity / leakage**: - `implementation(project(":lib"))` — `:lib` is an **internal** dependency. It's on the consumer's own classpath but is **not** exposed to projects that depend on the consumer. They cannot compile against `:lib`'s types. - `api(project(":lib"))` — `:lib` is part of the consumer's **public API**. It's placed on the consumer's `apiElements` configuration, so downstream consumers also get `:lib` on their compile classpath. `api` requires the **`java-library`** plugin (the plain `java` plugin has no `api` configuration). The rule of thumb: use `api` only when `:lib`'s types appear in your **public** signatures (return types, public method parameters, public superclasses); otherwise use `implementation` to minimise the ABI surface, reduce recompilation, and keep module boundaries clean.
code
kotlin · 8 linesplugins { `java-library` }
dependencies {
// :model types appear in :service's public method signatures
api(project(":model"))
// :util used only internally — hide it from consumers
implementation(project(":util"))
}go deeper
Know that api exposes the dependency to consumers and implementation hides it; default to implementation.
Explain the compile-vs-runtime distinction (apiElements vs runtimeElements) and the public-signature rule for choosing api.
Discuss recompilation blast radius / ABI propagation and how implementation shrinks the compile graph for faster incremental builds.
Treat api/implementation as an architectural governance tool across many modules; consider linting/conventions plugins that flag accidental api leakage.
## The two configurations The `java-library` plugin splits compile dependencies into two buckets: - **`api`** — dependencies that are part of *your module's public API*. They leak to consumers. - **`implementation`** — internal dependencies. They're on *your* classpath only and are hidden from consumers. This applies identically to **project dependencies**: ```kotlin // service/build.gradle.kts plugins { `java-library` } dependencies { api(project(":model")) // :service exposes :model's types implementation(project(":util")) // :util is hidden from :service's consumers } ``` ## What "public API" means here Use `api` when a type from the dependency appears in a **public or protected** signature of your module — a return type, a parameter, a public field, a thrown checked exception, or a superclass/interface. A consumer that calls those methods needs that type on its own compile classpath, so it must be exposed. Use `implementation` when the dependency is purely internal — used only inside method bodies or private members. Consumers never reference it, so it stays hidden. ## Why it matters beyond compilation 1. **Smaller compile classpaths** — `implementation` dependencies are kept off downstream compile classpaths, so fewer types are visible and accidental coupling is prevented. 2. **Faster builds / less recompilation** — changing the *implementation-only* internals of `:lib` (when it's behind `implementation`) does not force recompilation of the consumer's consumers, because the ABI they see is unchanged. With `api`, ABI changes propagate further. 3. **Cleaner architecture** — `implementation` enforces encapsulation between modules. ## The mechanics: configurations Under the hood the `java-library` plugin creates outgoing configurations: - `apiElements` — what consumers get at **compile** time (includes `api` deps). - `runtimeElements` — what consumers get at **runtime** (includes both `api` and `implementation` deps — implementation deps are still needed to *run*, just not to *compile against*). So `implementation` deps are runtime-transitive but **not** compile-transitive. ## Common mistake Using the plain `java` plugin and trying `api(...)` — it fails because `java` only provides `implementation`. Apply `java-library` to get the `api` configuration.
- If :app depends on :service via implementation, and :service depends on :util via implementation, can :app compile against :util's types?No. Both hops are implementation, so :util is hidden at compile time from :app. :util is still on :app's runtime classpath (runtime is transitive), but :app cannot reference :util's types in its source.
- Why does over-using api hurt build performance?api dependencies leak onto consumers' compile classpaths, so an ABI change in a deep dependency triggers recompilation of more downstream modules. implementation cuts that chain, shrinking the recompilation blast radius.
- Which plugin do you need for api(...) and why?java-library. The plain java plugin only defines implementation/compileOnly/runtimeOnly; java-library adds the api configuration plus the apiElements outgoing variant that carries api deps to consumers.
saying these in an interview costs you the question
- Claiming implementation dependencies are not available at runtime to consumers — they are (runtime is transitive); they're only hidden at compile time.
- Saying api works with the plain java plugin.
- Defaulting everything to api 'to be safe' — it defeats encapsulation and slows incremental builds.