skip to content

Explain when you would use `compileOnly` versus `runtimeOnly` when declaring a dependency, and give a realistic example of each.

level: middleimportance: must knowfreq 62%

answer

  1. compileOnly = compile, not runtime
  2. runtimeOnly = runtime, not compile
  3. Lombok / servlet-api = compileOnly
  4. JDBC driver / logback binding = runtimeOnly
  5. compileOnlyApi for consumer-visible compile-only

basics

~20 s

compileOnly puts a dependency on the compile classpath but not runtime (e.g. annotation libraries, provided-by-container APIs). runtimeOnly puts it on the runtime classpath but not compile (e.g. a JDBC driver or SLF4J binding loaded by reflection).

solid answer

~40 s

These two configurations are complementary opposites. `compileOnly` makes a dependency available **only at compile time** — it is not packaged and not on the runtime classpath. Classic uses: annotations you only need during compilation (e.g. `org.projectlombok:lombok`, JSR-305 `javax.annotation`), or APIs that the runtime environment provides (the old 'provided' scope, e.g. a servlet API supplied by the app server). `runtimeOnly` is the inverse: the dependency is **only on the runtime classpath**, never the compile classpath. You use it for implementations you don't reference in source but that must be present when the app runs — a JDBC driver (`org.postgresql:postgresql`) loaded by `DriverManager`, an SLF4J binding like `logback-classic`, or a logging implementation chosen at runtime. Keeping these out of the compile classpath prevents code from accidentally importing implementation classes and keeps the compile classpath lean.

code

kotlin · 12 lines
kotlin
dependencies {
    // needed by javac, not at runtime
    compileOnly("org.projectlombok:lombok:1.18.32")
    annotationProcessor("org.projectlombok:lombok:1.18.32")

    // compile against the API, bind the impl only at runtime
    implementation("org.slf4j:slf4j-api:2.0.13")
    runtimeOnly("ch.qos.logback:logback-classic:1.5.6")

    // loaded reflectively by DriverManager -> not referenced in source
    runtimeOnly("org.postgresql:postgresql:42.7.3")
}

go deeper

for a junior

Know compileOnly = compile but not run; runtimeOnly = run but not compile, with one example each.

for a middle

Give the two-classpath model and real examples (Lombok, servlet-api, JDBC driver, logback binding).

for a senior

Discuss compileOnlyApi, transitivity/non-transitivity, and how runtimeOnly enforces API-vs-impl separation for swappable bindings.

for a principal

Frame as packaging/conflict policy (provided deps, container responsibilities) and dependency-hygiene standards across teams.

## Two classpaths, two configurations A Java module in Gradle has (at least) two distinct classpaths: - the **compile classpath** — what `javac` sees when building your source - the **runtime classpath** — what's on the classpath when the program actually runs Most dependencies (`implementation`, `api`) are on **both**. The two configurations here let you opt into exactly one. ### `compileOnly` — compile classpath only The dependency is visible to the compiler but is **not** packaged into your artifact and **not** on the runtime classpath. Use it when: - **Annotations consumed at compile time**: Lombok, `@Nullable`/`@NonNull` (JSR-305), `@Generated`. The annotation processor or the annotations themselves are needed to compile, but the JAR isn't needed at runtime. - **'Provided' APIs**: the runtime container already supplies the implementation. The textbook case is `jakarta.servlet:jakarta.servlet-api` when deploying a WAR to a servlet container — the container provides it, so bundling it would cause conflicts. Note: `compileOnly` dependencies are **not transitive** to consumers and are not on the test classpaths automatically. There is a separate `compileOnlyApi` (java-library) for compile-only deps that must also be visible to consumers' compile classpath (e.g. an annotation used in your public API). ### `runtimeOnly` — runtime classpath only The dependency is packaged / on the runtime classpath but **invisible to the compiler**. Use it when you never reference the classes in source because they're loaded indirectly: - **JDBC drivers** loaded via `DriverManager` / connection URL - **Logging bindings**: you compile against the SLF4J *API*, and add the *binding* (`logback-classic` or `slf4j-simple`) as `runtimeOnly` so source can't accidentally import binding-internal classes - **Service-provider implementations** discovered via `ServiceLoader` Putting these on `runtimeOnly` guarantees code can't compile against implementation details, so you can swap the implementation freely. ```kotlin dependencies { compileOnly("org.projectlombok:lombok:1.18.32") annotationProcessor("org.projectlombok:lombok:1.18.32") implementation("org.slf4j:slf4j-api:2.0.13") runtimeOnly("ch.qos.logback:logback-classic:1.5.6") runtimeOnly("org.postgresql:postgresql:42.7.3") } ``` ### Quick mental table | Configuration | Compile classpath | Runtime classpath | |---|---|---| | `implementation` | yes | yes | | `compileOnly` | yes | no | | `runtimeOnly` | no | yes |

  • If you need a compile-only annotation to also be visible to consumers of your library, which configuration do you use?
    `compileOnlyApi` (provided by `java-library`). Plain `compileOnly` is not exposed to consumers.
  • Why declare an SLF4J binding as `runtimeOnly` instead of `implementation`?
    So source code can't accidentally import binding-internal classes; you compile only against the SLF4J API and can swap bindings (logback, slf4j-simple) without code changes.

saying these in an interview costs you the question

  • Putting a JDBC driver on `implementation` and importing the driver class directly in source.
  • Bundling a container-provided API (e.g. servlet-api) at runtime, causing classloader conflicts.
  • Confusing `compileOnly` (not packaged) with `runtimeOnly` (not compiled against).

context