Explain when you would use `compileOnly` versus `runtimeOnly` when declaring a dependency, and give a realistic example of each.
answer
- compileOnly = compile, not runtime
- runtimeOnly = runtime, not compile
- Lombok / servlet-api = compileOnly
- JDBC driver / logback binding = runtimeOnly
- compileOnlyApi for consumer-visible compile-only
basics
~20 scompileOnly 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 sThese 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 linesdependencies {
// 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
Know compileOnly = compile but not run; runtimeOnly = run but not compile, with one example each.
Give the two-classpath model and real examples (Lombok, servlet-api, JDBC driver, logback binding).
Discuss compileOnlyApi, transitivity/non-transitivity, and how runtimeOnly enforces API-vs-impl separation for swappable bindings.
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).