skip to content

Explain how a source set's compile and runtime classpaths relate to its `*Implementation`, `*CompileOnly`, and `*RuntimeOnly` configurations.

level: middleimportance: must knowfreq 60%

answer

  1. implementation = compile + runtime
  2. compileOnly off runtime, runtimeOnly off compile
  3. compileClasspath/runtimeClasspath are resolvable
  4. buckets canBeResolved=false
  5. api exported via apiElements

basics

~10 s

For a source set, compileClasspath resolves from its *CompileClasspath configuration (fed by implementation + compileOnly), and runtimeClasspath from its *RuntimeClasspath configuration (fed by implementation + runtimeOnly).

solid answer

~40 s

Each source set has two resolvable classpath configurations: `<set>CompileClasspath` and `<set>RuntimeClasspath`. You don't declare dependencies on those directly — they are resolvable-only. Instead you declare into the *bucket* configurations: `implementation` (visible at both compile and runtime), `compileOnly` (compile only, e.g. annotation libraries), and `runtimeOnly` (runtime only, e.g. JDBC drivers). With `java-library` you also get `api` (like `implementation` but exported to consumers). `compileClasspath` extends from `implementation` + `compileOnly`; `runtimeClasspath` extends from `implementation` + `runtimeOnly`. The `SourceSet.compileClasspath`/`runtimeClasspath` properties are `FileCollection`s pointing at those resolved configurations plus, for `test`, `main`'s output. This separation enforces that `compileOnly` deps never leak to runtime and `runtimeOnly` deps never pollute the compile API.

code

kotlin · 9 lines
kotlin
// Custom source set 'integrationTest' gets prefixed buckets automatically
sourceSets { create("integrationTest") }

dependencies {
    "integrationTestImplementation"("org.testcontainers:testcontainers:1.19.0")
    "integrationTestRuntimeOnly"("org.postgresql:postgresql:42.7.0")
}
// integrationTestCompileClasspath extends integrationTestImplementation + integrationTestCompileOnly
// integrationTestRuntimeClasspath extends integrationTestImplementation + integrationTestRuntimeOnly

go deeper

for a junior

Know implementation is for both compile and runtime; recognize compileOnly and runtimeOnly exist.

for a middle

Explain the extendsFrom wiring and which bucket feeds which resolvable classpath.

for a senior

Discuss bucket vs. resolvable vs. consumable roles and the ABI cost of api.

for a principal

Govern dependency hygiene across modules — minimizing api leakage to preserve build avoidance and clean module boundaries.

## Buckets vs. resolvable configurations Gradle splits configurations by role: - **Dependency-scope ('bucket') configurations** — where you *declare* dependencies. Examples: `implementation`, `compileOnly`, `runtimeOnly`, `api`. These are `canBeResolved = false`, `canBeConsumed = false`. - **Resolvable configurations** — what Gradle *resolves* into an actual classpath. Examples: `compileClasspath`, `runtimeClasspath`. These are `canBeResolved = true`. - **Consumable configurations** — what gets *published/exposed* to other projects, e.g. `apiElements`, `runtimeElements` (`canBeConsumed = true`). ## The wiring (for source set `main`) ``` implementation ──┐ compileOnly ──┴─► compileClasspath (resolvable) implementation ──┐ runtimeOnly ──┴─► runtimeClasspath (resolvable) ``` Concretely: `compileClasspath.extendsFrom(implementation, compileOnly)` and `runtimeClasspath.extendsFrom(implementation, runtimeOnly)`. For an arbitrary source set `foo`, the names gain the prefix: `fooImplementation`, `fooCompileClasspath`, etc. ## Why the split exists - `compileOnly` keeps things like `jakarta.annotation` or `lombok` off the runtime classpath, so they aren't shipped. - `runtimeOnly` keeps things like a JDBC driver off the compile classpath, so you can't accidentally code against the implementation. - `api` (java-library only) is like `implementation` but also lands in the consumable `apiElements`, leaking onto downstream consumers' compile classpath — use sparingly to avoid bloating the ABI. ## SourceSet view `SourceSet.getCompileClasspath()` returns a `FileCollection` that you can read or even augment (e.g. `sourceSets.test.get().compileClasspath += sourceSets.main.get().output`). The `test` set already has `main.output` folded into both classpaths by convention. ```kotlin dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0") // compile + runtime compileOnly("org.projectlombok:lombok:1.18.32") // compile only runtimeOnly("org.postgresql:postgresql:42.7.0") // runtime only } ```

  • Why can't you declare a dependency directly on `compileClasspath`?
    It's a resolvable-only configuration (`canBeResolved = true`, `canBeConsumed = false`) representing a resolved graph. Declaring there bypasses the bucket roles; you declare into `implementation`/`compileOnly` which it extends from.
  • What's the practical difference between `api` and `implementation` for the consumer's classpath?
    `api` dependencies are exported via `apiElements`, so they appear on consumers' compile classpath; `implementation` dependencies stay internal and only reach consumers at runtime, shrinking the compile-time ABI and improving build avoidance.

saying these in an interview costs you the question

  • Saying `compileOnly` dependencies are available at runtime — they are deliberately excluded from `runtimeClasspath`.
  • Treating `compile`/`runtime` (legacy) as current — they were removed; use `implementation`/`api`/`runtimeOnly`/`compileOnly`.
  • Declaring dependencies directly on `compileClasspath`/`runtimeClasspath`.

context