Show how you would use subprojects{} to apply a common plugin, repositories, and a shared test dependency across all modules, and explain the configuration-name gotcha.
answer
- apply(plugin=...) + repositories + dependencies
- string-named configurations in root
- configure<JavaPluginExtension> not java{}
- configureEach stays lazy
- every module gets it whether needed or not
basics
~10 sPut apply(plugin = "..."), a repositories {} block, and a dependencies {} block inside subprojects {}. Reference configurations like implementation by their string name because typed accessors aren't generated in the root script.
solid answer
~40 sInside `subprojects {}` you call the same configuration APIs you'd use in a leaf script, but against each child project. A typical block applies a language plugin, declares repositories, and adds shared dependencies. The gotcha is **configuration accessors**. In a normal subproject script that applies the `java` plugin, Gradle generates typed Kotlin accessors so you can write `implementation("...")`. In the root script those accessors don't exist for the *child's* configurations, so you must use the stringly-typed form: `"implementation"("...")`, `add("implementation", "...")`, or `dependencies { "testImplementation"(...) }`. The same applies to extension accessors like `java { }` — you may need `configure<JavaPluginExtension> { }` instead. This friction is one of the practical reasons teams migrate this logic into convention plugins, where the plugin is applied directly and the typed DSL works normally.
code
kotlin · 12 linessubprojects {
apply(plugin = "java-library")
repositories { mavenCentral() }
dependencies {
"testImplementation"("org.junit.jupiter:junit-jupiter:5.10.0")
"testRuntimeOnly"("org.junit.platform:junit-platform-launcher")
}
tasks.withType<Test>().configureEach { useJUnitPlatform() }
configure<JavaPluginExtension> {
toolchain { languageVersion.set(JavaLanguageVersion.of(21)) }
}
}go deeper
Recall that you place apply/repositories/dependencies inside subprojects{} to share them; the string-vs-typed nuance can be hand-waved.
Demonstrate the block AND explain the configuration-accessor gotcha plus the configure<JavaPluginExtension> workaround.
Add eager-ordering reasoning, configureEach laziness, and articulate why unconditional application is fragile.
Discuss the maintenance cost across dozens of modules and the migration path to opt-in convention plugins.
## The pattern A common goal: every code module should use Kotlin/Java, resolve from Maven Central, and have JUnit 5 on the test classpath. With cross-configuration you write it once in the root: ```kotlin subprojects { apply(plugin = "java-library") repositories { mavenCentral() } dependencies { "implementation"("com.google.guava:guava:33.0.0-jre") "testImplementation"("org.junit.jupiter:junit-jupiter:5.10.0") "testRuntimeOnly"("org.junit.platform:junit-platform-launcher") } tasks.withType<Test>().configureEach { useJUnitPlatform() } } ``` ## Why the strings? In the Kotlin DSL, Gradle generates **type-safe accessors** for configurations and extensions, but only for a build script where the contributing plugin is applied *directly in that script*. When you apply `java-library` to the *target* project from inside `subprojects {}`, the root script itself never had the plugin applied, so it gets no `implementation` accessor. Three ways around it: - String invoke: `"implementation"("…")` - Explicit add: `dependencies.add("implementation", "…")` - For extensions: `configure<JavaPluginExtension> { sourceCompatibility = JavaVersion.VERSION_21 }` instead of `java { }`. ## Eager apply ordering matters Because `subprojects {}` runs eagerly during configuration, the `apply(plugin = ...)` happens before the `dependencies {}` lines in the same block, so the `implementation` configuration exists by the time you reference it. If you split plugin application and dependency declaration across separate `subprojects {}` blocks, ordering within the root script still holds because all run during configuration of the root. ## tasks.withType and configureEach Note `tasks.withType<Test>().configureEach { }` — using `configureEach` keeps configuration lazy per task and avoids realizing tasks you don't need. This is a good habit even inside cross-configuration blocks. ## Why this is fragile Every subproject now silently gets `java-library`, Guava, and JUnit whether it needs them or not. A subproject that is, say, a pure Kotlin DSL module or a documentation aggregator inherits irrelevant plugins. That implicit, all-or-nothing application is the maintainability cost that convention plugins solve by letting each module opt in to a named convention.
- Why can't you just write `implementation("...")` in the root subprojects block like you do in a leaf script?The typed `implementation` accessor is only generated for scripts where the java plugin is applied directly. The root script doesn't apply it to itself, so no accessor exists and you fall back to the string-keyed `"implementation"(...)` form.
- A subproject doesn't need the java-library plugin but it's an aggregator. What happens with this block?It still gets java-library applied because subprojects{} is unconditional. You'd have to add an `if`/filter, or better, move to convention plugins each module opts into.
saying these in an interview costs you the question
- Insisting the typed `implementation(...)` accessor works in the root script — it doesn't for child configurations.
- Forgetting that subprojects{} applies the plugin to every module regardless of fit.