You want short logical paths like ':auth' and ':billing' but the folders grouped under services/ on disk. How do you wire that in settings?
answer
- flat include(":auth") + projectDir services/auth
- no synthetic :services parent
- loop over module names
- trade-off: concise vs group addressing
- file() resolves against rootDir
basics
~10 sInclude each project with a flat path and override projectDir to the grouped folder: include(":auth"); project(":auth").projectDir = file("services/auth"). The path stays flat while the directory is nested under services/.
solid answer
~40 sIf you `include(":services:auth")` the path is nested AND the folder is nested (`services/auth`). To keep the folder nested but the **path flat** (`:auth`), declare the flat path and relocate the directory: ```kotlin listOf("auth", "billing", "catalog").forEach { name -> include(":$name") project(":$name").projectDir = file("services/$name") } ``` Now dependencies read cleanly as `project(":auth")` while the disk stays organised under `services/`. The trade-off: flat paths are shorter and resolve faster to type, but you lose the path-based grouping (no `:services` parent, so you can't address the group as a unit in `tasks` or `subprojects` filters by path prefix). Choose based on whether you value concise references or hierarchical addressing more.
code
kotlin · 8 lines// settings.gradle.kts
rootProject.name = "shop"
listOf("auth", "billing", "catalog").forEach { name ->
include(":$name")
project(":$name").projectDir = file("services/$name")
}
// paths: :auth :billing :catalog ; folders: services/auth ...go deeper
Recognise you can include a flat path and point projectDir at a nested folder.
Write the loop, explain no parent project is created, and name the file(...) resolution rule.
Articulate the trade-off between concise references and hierarchical group addressing and when each wins.
Set a monorepo-wide convention for path vs directory shape and document the aggregation strategy you're forgoing or replacing.
## The coupling you're breaking Gradle's default ties the **logical path** to the **directory tree**: `include(":services:auth")` gives you both a nested path (`:services:auth`) and a nested folder (`services/auth`), and it implicitly creates the intermediate `:services` project. Sometimes you want the tidy on-disk grouping without paying for the verbose path or the synthetic parent project. ## Decoupling with projectDir Declare the project at the **path you want** and then point its directory at the folder you want: ```kotlin include(":auth") project(":auth").projectDir = file("services/auth") ``` A loop scales this to many modules: ```kotlin listOf("auth", "billing", "catalog").forEach { name -> include(":$name") project(":$name").projectDir = file("services/$name") } ``` Result: paths `:auth`, `:billing`, `:catalog` (flat), directories `services/auth`, etc. (grouped). No `:services` parent project is created because you never included a `:services:*` path. ## Trade-offs **Flat paths** give concise dependency references (`project(":auth")`) and slightly less typing. But hierarchical paths buy you **group addressing**: with `:services:auth` and `:services:billing` you can run `gradle :services:build`-style aggregation and filter `subprojects` by the `:services` path. Going flat sacrifices that grouping. If you later need group operations you'd reintroduce the parent or use plugin-based aggregation instead. ## file(...) resolution `file("services/auth")` resolves relative to the settings file's directory (the root). Always use `file(...)` rather than a raw string so the path is interpreted as a `File` against `rootDir`, keeping it portable across machines. ## When this pattern shines Monorepos with dozens of modules where teams want short, memorable references but still want directories grouped by domain or layer. It also helps migrations: you can flatten paths in the build graph without physically moving folders, or group folders without rewriting every `project(":...")` reference.
- Does this approach create a :services parent project?No. You only include flat paths like :auth, so Gradle never synthesizes a :services intermediate project. The grouping is purely physical on disk.
- What do you give up by going flat instead of include(":services:auth")?Path-based group addressing — you can't target or filter the whole :services group by path prefix or run an aggregate at a :services parent.
- Why use file("services/auth") rather than the plain string?file(...) resolves the path as a File relative to rootDir, making it portable; projectDir is a File property, so a String would not assign directly.
saying these in an interview costs you the question
- Assuming a :services parent project still exists with flat paths
- Hardcoding absolute paths instead of file(...) relative to rootDir