skip to content

In the `java-library` plugin, what is the difference between the `api` and `implementation` configurations?

level: middleimportance: must knowfreq 85%

answer

  1. api leaks to consumer compile classpath
  2. implementation = internal, runtime-only for consumers
  3. compile avoidance → faster builds
  4. type in public signature → api
  5. java-library required for api

basics

~10 s

api dependencies leak onto consumers' compile classpath (they're part of your public API); implementation dependencies are internal — visible at your own compile/runtime but hidden from consumers' compile classpath, only present at runtime.

solid answer

~40 s

The `java-library` plugin adds the `api` configuration on top of what `java` gives you. **`api`** declares dependencies that appear in your module's public surface — types you expose in method signatures, return types, or extended classes. They're put on **consumers' compile classpath** so callers can use those types. **`implementation`** is for internal dependencies that don't appear in your public API; consumers get them only **transitively at runtime**, never on their compile classpath. The win is faster builds and better encapsulation: changing an `implementation` dependency doesn't force recompilation of consumers, and you can't accidentally rely on a library's transitive deps. Rule of thumb: if a type from a dependency appears in a public/protected signature, use `api`; otherwise use `implementation`. Plain `java` only offers `implementation` (the `api` configuration requires `java-library`).

code

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

dependencies {
    // Exposed in public method signatures -> goes on consumers' compile classpath
    api("com.google.guava:guava:33.0.0-jre")

    // Internal detail -> consumers see it only at runtime
    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
}

go deeper

for a junior

Know that implementation hides a dependency from consumers and api exposes it; the precise variant mechanics can come later.

for a middle

Articulate the compile-classpath vs runtime-classpath distinction and the 'type in public signature → api' rule.

for a senior

Explain compile avoidance, encapsulation benefits, and the apiElements/runtimeElements variants behind it.

for a principal

Discuss governing api usage across an org to keep public surfaces minimal and protect incremental-build performance at scale.

## The two plugins - The `java` plugin gives every consumer-facing dependency the same treatment via `implementation` (internal) and offers no way to express "this is part of my public API." - The `java-library` plugin (which itself applies `java`) adds the **`api`** configuration, letting a library distinguish its **public** dependencies from its **internal** ones. ## api vs implementation Gradle models a library as producing two distinct things for consumers: - the **compile classpath** consumers need to compile against your API, and - the **runtime classpath** they need to actually run. `api` dependencies are added to **both** — they're transitively visible when consumers compile. `implementation` dependencies are added **only to runtime** for consumers — present when the app runs, absent when the consumer compiles. ## Why it matters: compile avoidance Because `implementation` deps don't appear on consumers' compile classpath, changing one (a version bump or even an ABI change deep in the graph) does **not** invalidate downstream compilation. This is **compile avoidance** and it speeds up large multi-module builds significantly. It also enforces **encapsulation**: a consumer can't compile against a transitive dependency it never declared. ## The decision rule Use `api` when a dependency's types leak into your public surface: ```kotlin plugins { `java-library` } dependencies { // Guava's types appear in this module's public method signatures api("com.google.guava:guava:33.0.0-jre") // Jackson is used only internally to (de)serialize, not exposed implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0") } ``` If you return a `com.google.common.collect.ImmutableList` from a public method, callers must compile against Guava → `api`. If you only call Jackson inside private methods → `implementation`. ## Under the hood These map to Gradle's outgoing **variants** via consumable configurations: `apiElements` (compile) carries `api` deps; `runtimeElements` (runtime) carries both `api` and `implementation`. The attribute `org.gradle.usage` (`java-api` vs `java-runtime`) selects which variant a consumer resolves.

  • Why is overusing `api` considered a smell?
    Every `api` dependency becomes part of your contract and is forced onto every consumer's compile classpath, defeating compile avoidance and leaking implementation choices. Prefer `implementation` unless a type is genuinely public.
  • Can you use `api` with the plain `java` plugin?
    No. The `api` configuration is added by `java-library`. With only `java` applied there's no `api`, so you must apply `java-library` to express public dependencies.

api is the wiring exposed on the wall socket — anything plugged into you must match it. implementation is the wiring inside the wall: it powers the device but nobody downstream needs to know it exists.

saying these in an interview costs you the question

  • Saying `api` and `implementation` differ only at runtime — the key difference is consumers' compile classpath.
  • Claiming plain `java` supports `api`.
  • Defaulting everything to `api` 'to be safe'.

context