skip to content

How do extra properties set on the root project become visible to subprojects, and what are the scoping rules?

level: middleimportance: should knowfreq 40%

answer

  1. per-object ext, not one global bag
  2. Groovy walks parent chain by simple name
  3. Kotlin: explicit rootProject.extra
  4. subprojects{} vs allprojects{}
  5. convenience, not real inheritance/copy

basics

~20 s

Extra properties live on the object they're set on. A property set on the root project is reachable from subprojects via rootProject.extra or property lookup that walks up the project hierarchy; it is not automatically copied into each subproject's own ext.

solid answer

~50 s

Each Gradle object has its own `ext`, so scoping is per-object. When you set `ext.foo` on the root project, it is stored on the root project's `ExtraPropertiesExtension`. Subprojects can read it because Gradle's dynamic property resolution for a project walks up the parent chain: an unqualified `foo` in a subproject that isn't found locally is searched on ancestor projects, eventually the root. In the Kotlin DSL there is no implicit walk for the `by extra` delegate, so you typically read it explicitly via `rootProject.extra["foo"]` or re-declare `val foo: String by extra` inside a `subprojects { }` / `allprojects { }` block configured from the root. A common pattern is defining shared versions in the root's `ext` and consuming them in `subprojects { }`. Note this is a configuration-time convenience, not true inheritance — the value is not duplicated onto each child's ext.

code

kotlin · 10 lines
kotlin
// root build.gradle.kts
val springVersion by extra("6.1.0")

subprojects {
    repositories { mavenCentral() }
    dependencies {
        // read the root's extra explicitly in the Kotlin DSL
        "implementation"("org.springframework:spring-core:${rootProject.extra["springVersion"]}")
    }
}

go deeper

for a junior

Know that a value set on the root is reachable from subprojects, typically defined once at the top.

for a middle

Explain per-object ext, the Groovy property walk vs explicit rootProject.extra in Kotlin, and allprojects vs subprojects.

for a senior

Note it is configuration-time convenience not real inheritance, and the configuration-cache/cross-project-config trade-offs.

for a principal

Advocate replacing pervasive cross-project ext sharing with convention plugins and version catalogs for isolation and scalability.

## Scoping is per-object There is no single global property bag. `project.ext`, `rootProject.ext`, and each subproject's `ext` are distinct `ExtraPropertiesExtension` instances. So "inheritance" is really about **resolution rules**, not copying. ## Groovy: the property walk When Groovy evaluates an unqualified name like `springVersion` in a subproject build script, Gradle's `Project` property resolution checks, in order: the project's own properties/ext, then ancestor projects up to the root, then `gradle.properties`-derived project properties, etc. This means a value placed in the root's `ext` is transparently visible by simple name in children: ```groovy // root build.gradle ext { springVersion = '6.1.0' } // any subproject build.gradle dependencies { implementation "org.springframework:spring-core:${springVersion}" // resolves to root } ``` ## Kotlin: be explicit The statically typed Kotlin DSL has no dynamic walk for delegates. You read the parent's value deliberately: ```kotlin // root build.gradle.kts val springVersion by extra("6.1.0") // subproject build.gradle.kts val springVersion: String by extra // resolves against THIS project's ext ``` The last line actually resolves against the subproject's own `extra` — which works only if the value is also present there. So the idiomatic Kotlin approach is to configure children from the root: ```kotlin // root build.gradle.kts val springVersion by extra("6.1.0") subprojects { // 'this' is each subproject; read root's value explicitly val v = rootProject.extra["springVersion"] as String extra["springVersion"] = v // optionally propagate onto child ext } ``` Or simply reference `rootProject.extra["springVersion"]` wherever needed. ## allprojects vs subprojects - `allprojects { }` — runs the block against the root **and** every subproject. - `subprojects { }` — runs against every subproject but not the root. These blocks are the usual place to either set ext on children or consume a root-level value. ## Why this matters - It is **configuration-time** behaviour; nothing is shared at execution unless you wired it through tasks/providers. - Cross-project configuration like this is increasingly discouraged in favour of **convention plugins** and **version catalogs**, which give explicit, isolated, configuration-cache-friendly sharing. But understanding the ext walk explains a huge amount of legacy multi-module builds.

  • Is a root-level extra property physically copied into each subproject's ext?
    No. It stays on the root's ExtraPropertiesExtension. Subprojects resolve it via the property walk (Groovy) or explicit rootProject.extra access (Kotlin) unless you deliberately copy it.
  • What is the difference between allprojects {} and subprojects {} for setting ext?
    allprojects configures the root and every subproject; subprojects configures every subproject but not the root. Choose based on whether the root itself needs the value.
  • Why is the Kotlin `val x: String by extra` in a subproject not the same as inheriting the root value?
    That delegate resolves against the subproject's own extra, so it only works if the value also exists there. To get the root's value you must use rootProject.extra explicitly.

saying these in an interview costs you the question

  • Saying root ext properties are automatically copied to every child's ext.
  • Assuming the Groovy parent walk also applies to Kotlin delegates.
  • Recommending heavy allprojects/subprojects ext sharing without mentioning convention plugins / catalogs as the modern alternative.

context