In a Kotlin DSL dependencies { } block, what is the difference between implementation and api, and how do compileOnly, runtimeOnly, and testImplementation differ?
answer
- implementation = private; api = exposed transitively
- api needs the java-library plugin
- compileOnly: compile not runtime (Lombok/servlet)
- runtimeOnly: runtime not compile (JDBC/Logback)
- test* variants scope to the test source set
basics
~20 simplementation keeps a dependency private to your module; api exposes it to modules that depend on you. compileOnly is needed only to compile (not at runtime), runtimeOnly only at runtime (not to compile), and testImplementation only for tests.
solid answer
~40 sThese are **dependency configurations** the Java/`java-library` plugins contribute as type-safe accessors. `implementation` puts a library on your compile and runtime classpath but **does not leak it transitively**—consumers of your module can't see it, which speeds up incremental compilation and enforces encapsulation. `api` (only from the `java-library` plugin) is like `implementation` but **is** exposed transitively, so use it only when a type from that library appears in your public signatures. `compileOnly` is on the compile classpath but **not** packaged/runtime (e.g. annotation interfaces, provided servlet APIs). `runtimeOnly` is on the runtime classpath only, not visible at compile time (e.g. a JDBC driver or logging backend). `testImplementation` mirrors `implementation` but scoped to the test source set; siblings include `testRuntimeOnly` and `testCompileOnly`. Choosing `implementation` over `api` by default is the standard performance/architecture guidance.
code
kotlin · 10 linesplugins { `java-library` } // 'api' accessor needs java-library
dependencies {
api("com.google.guava:guava:33.0.0-jre") // appears in public signatures
implementation("org.slf4j:slf4j-api:2.0.12") // internal use only
compileOnly("org.projectlombok:lombok:1.18.32") // compile-time only
runtimeOnly("org.postgresql:postgresql:42.7.3") // loaded at runtime
testImplementation(kotlin("test"))
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.2")
}go deeper
Knows implementation is the usual choice and testImplementation is for tests.
Correctly contrasts implementation vs api and explains compileOnly/runtimeOnly with examples.
Articulates the transitive-leak and incremental-build implications and the java-library requirement for api.
Establishes module API-boundary conventions (api vs implementation discipline) to control build performance and coupling at scale.
## Configurations = dependency 'scopes' In the Kotlin DSL `dependencies { }` block you attach each dependency to a **configuration** (a named bucket). The Java plugins generate these as type-safe accessors. The choice controls **which classpath** the dependency lands on and **whether it leaks to consumers**. ```kotlin dependencies { api("com.google.guava:guava:33.0.0-jre") // exposed transitively implementation("org.slf4j:slf4j-api:2.0.12") // internal only compileOnly("org.projectlombok:lombok:1.18.32") // compile, not runtime runtimeOnly("org.postgresql:postgresql:42.7.3") // runtime, not compile testImplementation(kotlin("test")) // tests only testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.2") } ``` ## implementation vs api — the key distinction - **`implementation`**: the dependency is on **your** compile + runtime classpath, but it is **hidden from downstream consumers**. If module B depends on module A, and A uses Guava via `implementation`, B does **not** get Guava on its compile classpath transitively. Benefit: changing an `implementation` dependency does not force recompilation of consumers → faster incremental builds and cleaner encapsulation. - **`api`** (provided by the **`java-library`** plugin, not plain `java`): same classpaths, but the dependency **is** exposed transitively to consumers. Use it only when a type from the dependency appears in your **public API** (return types, parameters, public fields). Otherwise prefer `implementation`. > Rule of thumb: **default to `implementation`; promote to `api` only when the type is part of your published surface.** ## compileOnly On the **compile** classpath only — **not** packaged and **not** on the runtime classpath. Used for things present at runtime by other means or needed only to compile: annotation processors' annotation jars, `provided`-style APIs (servlet/JEE), or compile-time-only tools. A `compileOnlyApi` variant exposes it transitively for compilation. ## runtimeOnly On the **runtime** classpath only — **not** visible at compile time. Classic cases: a **JDBC driver** (you code to `java.sql`, the driver is loaded reflectively at runtime) or a **logging backend** (you compile against the SLF4J API, ship Logback as `runtimeOnly`). ## test-scoped variants Each source set has its own configurations. For the `test` source set: - **`testImplementation`** — like `implementation` but only for compiling/running tests. - **`testRuntimeOnly`** — runtime-only for tests (e.g. the JUnit Platform launcher). - **`testCompileOnly`** — compile-only for tests. ## Why this matters in the Kotlin DSL specifically Because these are type-safe accessors, the IDE autocompletes the available configurations and flags a misspelling at edit time. If an accessor like `api` is missing, you likely applied plain `java` rather than `java-library`, or applied the plugin via legacy `apply()`.
- Why does overusing api hurt build performance?api dependencies are exposed transitively, so changing one invalidates and recompiles all downstream consumers; implementation contains the change to the owning module.
- Your build says 'api' is unresolved in the dependencies block. Why?The api configuration comes from the java-library plugin. With only the plain java plugin (or a legacy apply()), the api accessor isn't generated.
- Where would you put a JDBC driver and why?runtimeOnly — you compile against the standard java.sql API and the concrete driver is needed only at runtime, keeping it off the compile classpath.
implementation is an ingredient cooked into the dish that guests never see; api is a side served on the plate that everyone downstream can taste too.
saying these in an interview costs you the question
- Using api by default / not knowing it leaks transitively
- Thinking api comes from the plain java plugin
- Confusing compileOnly and runtimeOnly directions
- Putting test dependencies in implementation instead of testImplementation
- Claiming implementation hides the dependency at runtime too (it doesn't — only from downstream consumers' compile classpath)