skip to content

What is the difference between the `implementation` and `api` dependency configurations in a Gradle java-library project, and when would you choose one over the other?

level: middleimportance: must knowfreq 78%

answer

  1. api leaks to consumer compile classpath
  2. implementation hidden from consumer compile
  3. rule: type in public API -> api
  4. api needs java-library plugin
  5. smaller classpath = faster compile

basics

~10 s

api exposes a dependency on the consumer's compile classpath (it leaks); implementation keeps it internal to the module. Use api only when the dependency appears in your public types, otherwise implementation.

solid answer

~40 s

Both `api` and `implementation` put a dependency on the **compile classpath of the declaring module**. The difference is what they do for *consumers* of that module. `api` dependencies are transitively exposed: a downstream project that depends on your library also gets the `api` dependency on *its* compile classpath. `implementation` dependencies are hidden — they're on your compile and runtime classpath, and on the consumer's *runtime* classpath, but **not** the consumer's compile classpath. The rule: use `api` only when a type from that dependency appears in your module's public API (a method return type, parameter, public field, or supertype). Everything else should be `implementation`. Keeping dependencies `implementation` shrinks the consumer's compile classpath, speeds up compilation, and reduces accidental coupling. `api` is only available via the `java-library` plugin, not the plain `java` plugin.

code

kotlin · 9 lines
kotlin
plugins { `java-library` }

dependencies {
    // A Guava type appears in a public method signature -> exposed
    api("com.google.guava:guava:33.0.0-jre")

    // Used only inside method bodies -> hidden from consumers
    implementation("org.apache.commons:commons-lang3:3.14.0")
}

go deeper

for a junior

Know that api exposes the dependency to consumers and implementation keeps it private; default to implementation.

for a middle

Explain the compile-vs-runtime classpath distinction and the 'type in public API' rule; mention java-library is required for api.

for a senior

Discuss build-performance implications (smaller compile classpaths, avoided recompilation), API governance, and migration from the legacy compile scope.

for a principal

Frame it as module-boundary and ABI-stability policy across a large multi-module build; how to enforce minimal api surfaces org-wide.

## The problem these configurations solve When module B depends on module A, and A in turn depends on library X, a question arises: should X be visible to B at *compile* time? In old Gradle (and Maven's `compile` scope) the answer was always yes — every transitive dependency leaked onto every consumer's compile classpath. That made builds slow (huge classpaths) and brittle (code accidentally compiled against transitive deps it never declared). Gradle's `java-library` plugin fixes this by splitting the old `compile` configuration into two: - **`api`** — a dependency that is part of your module's *exported* contract. It is placed on your compile classpath **and** transitively exposed to consumers' compile classpaths. - **`implementation`** — an internal dependency. It is on your compile and runtime classpaths and on consumers' *runtime* classpath, but it is **hidden** from consumers' compile classpath. ## The decision rule Ask: *does a type from this dependency appear anywhere in my public API?* Public API means: - a `public`/`protected` method return type or parameter type - a public field type - a superclass or implemented interface - a public annotation If yes → `api`. If the type only appears inside method bodies, private fields, or non-exported classes → `implementation`. ## Why it matters ``` consumer --implementation--> my-lib --implementation--> guava ``` With `implementation`, the consumer **cannot** `import com.google.common...` at compile time — Guava is invisible. If you later swap Guava for something else, no consumer breaks. With `api`, Guava is on the consumer's compile classpath, so consumers may start importing it, and now it's effectively part of your contract — you can't remove it without a breaking change. Smaller compile classpaths also mean **faster incremental compilation**: changing an `implementation` dependency's ABI does not force recompilation of downstream modules (Gradle can skip them because that dependency never affected their compile classpath). ## Availability `implementation`, `compileOnly`, `runtimeOnly`, `testImplementation`, etc. come from the `java` plugin. `api` is **only** added by the `java-library` plugin. Applying `java-library` is what unlocks the api/implementation separation. ```kotlin plugins { `java-library` } dependencies { api("com.google.guava:guava:33.0.0-jre") // a Guava type is in my public API implementation("org.apache.commons:commons-lang3:3.14.0") // internal helper only } ```

  • Which plugin do you need for the `api` configuration to exist?
    `java-library`. The plain `java` plugin provides `implementation`, `compileOnly`, `runtimeOnly`, etc., but not `api`.
  • If a consumer needs a dependency at runtime but you declared it `implementation`, does it still get it?
    Yes. `implementation` is hidden from the consumer's *compile* classpath but is present on the consumer's *runtime* classpath, so it's available at runtime.

api is like listing an ingredient on the front label of a product — customers see it and may depend on it. implementation is a back-kitchen ingredient: it's used to make the dish, but customers never know and you can swap it freely.

saying these in an interview costs you the question

  • Saying `implementation` is completely invisible to consumers — it is still on their runtime classpath.
  • Claiming `api` is available with the plain `java` plugin.
  • Defaulting everything to `api` 'to be safe' — that defeats the purpose and slows builds.

context