Show how a convention plugin can configure Java settings only when a project applies the java plugin, without forcing the java plugin onto every consumer. Walk through the mechanism.
answer
- don't plugins.apply(java) — forces it
- withPlugin(java) inside apply()
- extension/test task safe inside block
- consumer chooses java or not
- order-independent composition
basics
~10 sIn the convention plugin's apply(), wrap the Java config in pluginManager.withPlugin("java") { ... }. It runs only for consumers that apply java, and skips others — so the plugin doesn't force java on anyone.
solid answer
~50 sA convention plugin centralizes shared build logic. To make it composable, it should **react** rather than assume. Inside `apply(project)`, instead of calling `project.plugins.apply("java")` (which would force the java plugin on every consumer), you write `project.pluginManager.withPlugin("java") { ... }`. The block runs only for projects that actually apply the java plugin — immediately if java is already applied, or the moment it gets applied later — and is silently skipped for non-Java modules. Inside the block you safely touch the `java` extension, the `test` task, and java-specific configurations because they're guaranteed to exist there. This lets one convention plugin be applied uniformly across a multi-project build: Java modules get the Java conventions, a docs-only or platform module that applies the convention but not java simply gets nothing extra. It's the standard pattern for building a reusable, order-independent build platform.
code
kotlin · 11 linesclass JavaConventionsPlugin : Plugin<Project> {
override fun apply(project: Project) = with(project) {
repositories.mavenCentral()
pluginManager.withPlugin("java") {
extensions.configure<JavaPluginExtension> {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}
tasks.named<Test>("test") { useJUnitPlatform() }
}
}
}go deeper
Recall the pattern: wrap Java config in withPlugin("java") so it only runs for Java modules.
Explain why forcing the plugin is wrong and how withPlugin keeps the convention composable and order-independent.
Discuss structuring conventions into reactive blocks per capability and the buildSrc/build-logic placement.
Frame a build-platform design: a small set of reactive convention plugins applied uniformly, each lighting up only relevant capabilities, governed centrally.
## Convention plugins, briefly A *convention plugin* is build logic you extract once and apply to many projects — usually a precompiled script plugin (`my.java-conventions.gradle.kts` in `buildSrc`/`build-logic`) or a `Plugin<Project>` class. Its job is to set shared conventions: toolchain, test framework, repositories, compiler args. ## The forcing trap A naive convention plugin does: ```kotlin class JavaConventionsPlugin : Plugin<Project> { override fun apply(project: Project) { project.plugins.apply("java") // forces java everywhere project.extensions.configure<JavaPluginExtension> { /* ... */ } } } ``` Now **every** project that applies your convention plugin is forced to be a Java project. A documentation module or an aggregation/platform module that just wants your repositories or version pins is now wrongly a Java project. ## React instead ```kotlin class JavaConventionsPlugin : Plugin<Project> { override fun apply(project: Project) { // unconditional shared bits here (e.g. repositories) project.repositories.mavenCentral() // Java-specific bits only when java is actually applied project.pluginManager.withPlugin("java") { project.extensions.configure<JavaPluginExtension> { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) } project.tasks.named<Test>("test") { useJUnitPlatform() } project.dependencies.add( "testImplementation", "org.junit.jupiter:junit-jupiter:5.10.0" ) } } } ``` Now the consumer decides whether it's a Java project: ```kotlin // a Java module plugins { id("java"); id("my.java-conventions") } // a docs module — gets repositories but no Java conventions plugins { id("my.java-conventions") } ``` ## Why the block is safe Inside `withPlugin("java")`, the `java` extension, the `test` task, and the `implementation`/`testImplementation` configurations are guaranteed present, because the block runs *after* java applied them. Outside the block you must not touch them. ## Order independence It doesn't matter whether `id("java")` is listed before or after `id("my.java-conventions")` in the consumer's `plugins {}` block — `withPlugin` reacts either way. That removes a whole class of brittle ordering bugs and is what makes convention plugins safely composable across a large multi-project build.
- What would change if you used project.plugins.apply("java") at the top instead of reacting?Every consumer of the convention plugin would be forced into being a Java project, even docs/platform modules — losing composability.
- Does the order of id("java") vs id("my.java-conventions") in the consumer matter?No. withPlugin reacts whether java is applied before or after the convention plugin, so the consumer's plugins{} order is irrelevant.
- Where would you typically put such a convention plugin?In buildSrc or a build-logic included build, as a precompiled script plugin (*.gradle.kts) or a Plugin<Project> class, then publish/apply by id.
saying these in an interview costs you the question
- Forcing java via plugins.apply inside the convention plugin and calling it 'reacting'.
- Touching the java extension or test task outside the withPlugin block (it may not exist yet).