skip to content

How does include(':services:auth') build out the project tree, and what does it imply about intermediate projects?

level: middleimportance: should knowfreq 40%

answer

  1. include of deep path creates intermediates
  2. ':services' auto-created if absent
  3. intermediates have no build file, no outputs
  4. subprojects { } iterates over them too
  5. default mapping :a:b -> a/b directory

basics

~10 s

include(':services:auth') registers the leaf and implicitly creates any missing intermediate projects on the path (here ':services'). Each segment becomes a project node; intermediate ones exist even without their own build file.

solid answer

~50 s

When you call `include(":services:auth")` in `settings.gradle.kts`, Gradle parses the colon-separated path and ensures **every** segment exists as a project in the tree. If `:services` was never explicitly included, Gradle creates it implicitly as an intermediate (container) project. So a single `include` of a deep path can materialize several projects: the root, `:services`, and `:services:auth`. Intermediate projects are real `Project` instances — they appear in `subprojects`, can be configured, and map (by default) to the `services/` directory — but they typically have no `build.gradle.kts` of their own and produce no artifacts; they exist purely to give the leaf a place in the hierarchy. This has practical consequences: `subprojects { }` or `allprojects { }` blocks will iterate over those intermediate container projects too, so plugin-applying or dependency-adding logic must tolerate projects that are just structural. By default Gradle maps `:services:auth` to the directory `services/auth`, creating the implied folder structure expectation.

code

kotlin · 7 lines
kotlin
// settings.gradle.kts
rootProject.name = "platform"
include(":services:auth")     // creates :services (container) + :services:auth
include(":services:gateway")  // shares :services, adds :services:gateway

// :services has a path, a name ('services'), maps to services/ dir,
// is visited by subprojects { }, but typically has no build.gradle.kts.

go deeper

for a junior

Know that include(':services:auth') registers the leaf and that ':services' becomes a project too.

for a middle

Explain implicit intermediate creation, that they're real but usually empty, and that subprojects { } iterates over them.

for a senior

Discuss the maintenance hazard of blanket cross-project config over container projects and prefer convention plugins.

for a principal

Decide hierarchy depth/shape as an architectural concern, balancing logical grouping against the cost of empty container nodes and broad configuration.

## What include actually does `include(path)` in `settings.gradle.kts` takes an absolute project path and **registers each segment** as a project, creating any that don't already exist. For `include(":services:auth")`: 1. Root project `:` already exists. 2. `:services` is created if absent (an **implicit intermediate** project). 3. `:services:auth` is created. You can `include` the deep path directly without a separate `include(":services")` — Gradle fills the gap. ## Intermediate (container) projects These auto-created nodes are full projects: - They appear in `rootProject.subprojects` and are visited by `subprojects { }` / `allprojects { }`. - They have a `projectDir` (default `services/`), a path, and a name. - They usually have **no build file** and produce **no outputs** — they're structural. ```kotlin // settings.gradle.kts include(":services:auth") // creates :services AND :services:auth include(":services:gateway") // reuses existing :services, adds :services:gateway ``` Here `:services` is shared as the common parent of both leaves. ## Why it matters If your root script does: ```kotlin subprojects { apply(plugin = "java-library") } ``` that lambda also runs against `:services`, the empty container. Applying `java-library` to a folder with no sources is harmless but wasteful; more dangerous is logic that assumes every subproject has a `jar` or a specific source set. Guard such logic (e.g. check for a build file or use convention plugins applied per-leaf) rather than blanket `subprojects`. ## Default directory mapping By default `:services:auth` → `services/auth` relative to the root. Gradle does **not** require `services/` to contain a build file, but it does expect the directory to exist for the leaf. You can override with `project(":services:auth").projectDir = file("...")` if the physical layout differs from the logical path. ## Takeaway A hierarchical `include` is a convenience that builds the whole path; just remember the intermediate nodes are real, configurable, and iterated over — design cross-project configuration with that in mind.

  • Do you need a separate include(':services') if you already include(':services:auth')?
    No. Including the deep path implicitly creates the intermediate :services project. A separate include is only needed if you want it for clarity; it's idempotent.
  • Why can a blanket subprojects { apply(plugin = ...) } cause surprises with deep paths?
    Because auto-created intermediate container projects are visited too. Logic assuming each subproject has sources/outputs may misbehave; prefer convention plugins applied per leaf.

saying these in an interview costs you the question

  • Believing intermediate projects don't exist unless explicitly included — they're auto-created.
  • Assuming every subproject visited by subprojects { } is a buildable module with outputs.

context