skip to content

You want short logical paths like ':auth' and ':billing' but the folders grouped under services/ on disk. How do you wire that in settings?

level: middleimportance: should knowfreq 35%

answer

  1. flat include(":auth") + projectDir services/auth
  2. no synthetic :services parent
  3. loop over module names
  4. trade-off: concise vs group addressing
  5. file() resolves against rootDir

basics

~10 s

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

If 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
kotlin
// 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

for a junior

Recognise you can include a flat path and point projectDir at a nested folder.

for a middle

Write the loop, explain no parent project is created, and name the file(...) resolution rule.

for a senior

Articulate the trade-off between concise references and hierarchical group addressing and when each wins.

for a principal

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

context