skip to content

How do you declare dependencies in build.gradle.kts, and what is the role of configurations like implementation and api?

level: middleimportance: must knowfreq 60%

answer

  1. dependencies { implementation("g:n:v") }
  2. implementation = hidden from consumers
  3. api = leaks transitively (java-library)
  4. compileOnly / runtimeOnly
  5. project(":core"), libs.* catalog

basics

~10 s

Inside dependencies {}, attach coordinates to a configuration: implementation("group:name:version"), testImplementation(...), api(...). The configuration name decides the scope and whether the dependency leaks to consumers.

solid answer

~40 s

In the Kotlin DSL you declare dependencies inside the `dependencies {}` block by calling a **configuration** as a function and passing the coordinates: `implementation("com.google.guava:guava:33.0.0-jre")`. The configuration name encodes intent and visibility. `implementation` puts the dependency on the compile and runtime classpath of *this* module but hides it from consumers' compile classpath, improving encapsulation and build performance. `api` (from the `java-library` plugin) also exposes it transitively to consumers — use it only when the dependency appears in your public API. `compileOnly` is compile-time only (e.g. annotations); `runtimeOnly` is runtime only (e.g. a JDBC driver); `testImplementation` scopes to tests. With a version catalog you write `implementation(libs.guava)`, getting type-safe, IDE-completed accessors. Preferring `implementation` over `api` is the default best practice because it avoids leaking transitive dependencies.

code

kotlin · 8 lines
kotlin
dependencies {
    implementation("com.google.guava:guava:33.0.0-jre")
    api("org.apache.commons:commons-lang3:3.14.0") // only if in public API
    compileOnly("org.projectlombok:lombok:1.18.32")
    runtimeOnly("org.postgresql:postgresql:42.7.3")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
    implementation(project(":core"))
}

go deeper

for a junior

Know the dependencies {} block and the implementation/testImplementation forms.

for a middle

Distinguish implementation vs api vs compileOnly vs runtimeOnly and explain why implementation is the default.

for a senior

Reason about API leakage, recompilation cost, and the java-library plugin's separation of api/implementation.

for a principal

Set org conventions for dependency hygiene (catalogs, banning api unless justified) to keep large module graphs decoupled and fast.

## The dependencies {} block `dependencies {}` is where you declare what your module needs. In the Kotlin DSL each declaration is a function call: the **configuration name** is the function, and the dependency notation is the argument. ```kotlin dependencies { implementation("com.google.guava:guava:33.0.0-jre") api("org.apache.commons:commons-lang3:3.14.0") compileOnly("org.projectlombok:lombok:1.18.32") runtimeOnly("org.postgresql:postgresql:42.7.3") testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") } ``` ## What a configuration is A **configuration** is a named bucket of dependencies with a defined role on the classpath. Common ones provided by the `java`/`java-library` plugins: - **implementation** — needed to compile and run this module; **not** exposed to consumers' compile classpath. The default choice. - **api** — like implementation but **also** leaks transitively onto consumers' compile classpath. Requires the `java-library` plugin. Use only when the type appears in your public API (return types, parameters). - **compileOnly** — on the compile classpath only, not packaged or on the runtime classpath (annotations, provided APIs). - **runtimeOnly** — on the runtime classpath only, not compile (e.g. a JDBC driver chosen at runtime). - **testImplementation / testRuntimeOnly** — the same roles, scoped to the `test` source set. ## Why implementation vs api matters If you use `api`, every consumer of your module also compiles against that transitive dependency. That couples consumers to your internal choices and forces them to recompile when those internals change. `implementation` walls the dependency off, so the consumer's compile classpath stays smaller and rebuilds are faster. Rule of thumb: **default to `implementation`; use `api` only when the dependency is genuinely part of your published API.** ## Dependency notation - String notation: `"group:name:version"`. - Map-like named arguments are possible but the string form is idiomatic in Kotlin. - Project dependency: `implementation(project(":core"))`. - Version catalog: `implementation(libs.guava)` — generated, type-safe accessor. ## Repositories Dependencies are fetched from repositories declared in `repositories {}` (e.g. `mavenCentral()`), or from `dependencyResolutionManagement` in settings.

  • Why prefer implementation over api by default?
    implementation hides the dependency from consumers' compile classpath, reducing coupling and speeding up recompilation; api leaks it transitively, which should be intentional.
  • Which plugin must be applied to use the api configuration?
    The java-library plugin. The plain java plugin does not provide api.
  • When would you use runtimeOnly?
    For dependencies needed only at runtime, not compile time — e.g. a JDBC driver loaded by name, so it isn't on the compile classpath.

saying these in an interview costs you the question

  • Using api everywhere — it leaks transitive deps and slows builds.
  • Thinking compileOnly dependencies are packaged into the runtime artifact; they are not.

context