skip to content

Reacting to Applied Plugins

Configuring something only once another plugin has been applied, using pluginManager.withPlugin or plugins.withType. Interviewers ask because it removes the ordering assumptions that make plugins fragile.

on this pageshow

questions

5

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.

level: middleimportance: must knowfreq 50%

answer

  1. don't plugins.apply(java) — forces it
  2. withPlugin(java) inside apply()
  3. extension/test task safe inside block
  4. consumer chooses java or not
  5. order-independent composition

basics

~10 s

In 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 s

A 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 lines
kotlin
class 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

for a junior

Recall the pattern: wrap Java config in withPlugin("java") so it only runs for Java modules.

for a middle

Explain why forcing the plugin is wrong and how withPlugin keeps the convention composable and order-independent.

for a senior

Discuss structuring conventions into reactive blocks per capability and the buildSrc/build-logic placement.

for a principal

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).

context

open as a page

Why would you use pluginManager.withPlugin("java") { ... } instead of just configuring the java extension directly in your build script?

level: middleimportance: must knowfreq 60%

basics

~10 s

Because the java plugin might not be applied yet (or at all). withPlugin defers your configuration so it only runs when/if that plugin is present, instead of failing or forcing an apply order.

open as a page

You maintain an internal plugin that should add an integration-test source set and a JaCoCo report wiring, but only if both the java and jacoco plugins are present. How do you wire this reliably?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Nest reactions: react to java with withPlugin("java") to add the source set, and inside (or separately) react to jacoco with withPlugin("jacoco") to wire the report. Each block runs only when its plugin is present, so both conditions must hold.

open as a page

Some teams write afterEvaluate { if (plugins.hasPlugin("java")) { ... } } to configure things conditionally. Why is reacting with withPlugin/withType generally better?

level: seniorimportance: should knowfreq 35%

basics

~20 s

afterEvaluate delays all your config to the end of configuration and runs even if nothing changed; withPlugin runs exactly when the plugin appears, is lazy and ordering-safe, and avoids the timing pitfalls and lifecycle fragility of afterEvaluate.

open as a page

What's the difference between pluginManager.withPlugin("java") { } and plugins.withType<JavaPlugin> { }, and when would you pick one over the other?

level: seniorimportance: should knowfreq 45%

basics

~20 s

withPlugin keys off the plugin's id string; withType keys off its class. Use withPlugin for third-party plugins not on your classpath; use withType when you have the plugin type and want a typed, refactor-safe reference.

open as a page