When does a project's hierarchical path stop matching its directory layout, and how do you handle it deliberately?
answer
- path = logical, projectDir = physical
- override in settings.gradle.kts
- project(':a:b').projectDir = file(...)
- path stays stable, files relocate
- cost: discoverability/onboarding
basics
~20 sThe path is logical; the directory is physical. By default ':services:auth' maps to services/auth, but you can remap projectDir in settings.gradle.kts so the same path lives anywhere on disk, decoupling logical structure from physical layout.
solid answer
~50 sA hierarchical path like `:services:auth` is a **logical** identifier; the directory it maps to is a separate, **physical** concern. By convention Gradle derives the directory from the path (`services/auth` under the root), but the two are independent. In `settings.gradle.kts` you can override the directory: `project(":services:auth").projectDir = file("legacy/authentication")`. The project's path, name, dependency references, and task addressing all stay `:services:auth` regardless of where the files actually live. This decoupling is useful when migrating legacy layouts, when you want a clean logical grouping (`:services:*`) over a messy historical folder structure, or when a flat physical layout (via `includeFlat`-style overrides) is preferred while keeping nested logical paths. The trade-off is discoverability: developers expect path to mirror directory, so deviations should be documented and consistent. You can also override the build file name per project with `project(":services:auth").buildFileName`, though that's rarely advisable.
code
kotlin · 10 lines// settings.gradle.kts
rootProject.name = "platform"
include(":services:auth")
// Logical path stays :services:auth, files live elsewhere
project(":services:auth").projectDir = file("legacy/authentication-service")
// Everywhere else, references are unchanged:
// implementation(project(":services:auth"))
// ./gradlew :services:auth:testgo deeper
Know that by default :a:b maps to directory a/b, and that this is the normal case.
Explain that projectDir can be overridden in settings so the path stays while files move.
Reason about when decoupling is justified (legacy migration, flat-physical/nested-logical) and the discoverability trade-offs.
Treat the path as a stable inter-module contract and govern projectDir overrides as documented, sparing exceptions to keep layouts navigable across the org.
## Logical path vs physical directory Two orthogonal things: - **Path** — `:services:auth`, the node's position in the project tree; used for dependencies, configuration, and task invocation. - **projectDir** — the filesystem folder holding that project's sources and build script. By default Gradle computes `projectDir` from the path: each colon becomes a directory separator, rooted at the build root. So `:services:auth` → `<root>/services/auth`. ## Breaking the default mapping You override the directory in `settings.gradle.kts`, where the project tree is being assembled: ```kotlin include(":services:auth") project(":services:auth").projectDir = file("legacy/authentication-service") ``` Now the **path stays** `:services:auth` — every `project(":services:auth")`, `:services:auth:test`, and `implementation(project(":services:auth"))` is unchanged — but the files live in `legacy/authentication-service`. The logical contract is stable while the physical location floats. ## When you'd deliberately do this 1. **Legacy migration** — impose a clean `:domain:*` / `:services:*` logical hierarchy on top of an inherited, inconsistent directory tree without a risky mass move. 2. **Flat physical, nested logical** — keep all modules in a single flat directory for tooling reasons while still grouping them logically by path (related to `includeFlat`, but done per-project with `projectDir`). 3. **Co-locating with non-Gradle assets** — point a project at a directory shaped by another tool. ## Costs and conventions - **Discoverability** — engineers assume path mirrors directory. Surprising mappings slow onboarding; document them and keep them consistent. - **buildFileName override** — `project(":services:auth").buildFileName = "auth.gradle.kts"` is possible but fights conventions; avoid unless forced. - **Tooling** — IDE import and some plugins assume the conventional mapping; verify they cope with overrides. ## Verifying the mapping At configuration time, `project(":services:auth").projectDir` reports the resolved physical directory, and `.path` reports the logical path — useful for asserting layout in a settings or convention script. ## Principle Treat the path as the stable public contract (what other modules and CI reference) and the directory as an implementation detail you may relocate — but only with clear, documented intent.
- If you remap projectDir, do you need to change project dependency declarations?No. Dependencies, task paths, and configuration all key off the logical path (:services:auth), which is unchanged by a projectDir override.
- What's the main downside of decoupling path from directory?Discoverability — engineers expect path to mirror the folder. Non-obvious mappings hurt onboarding and can confuse IDE import/plugins, so document and minimize them.
- Where must the projectDir override be declared?In settings.gradle.kts, where the project tree is assembled — after the corresponding include for that path.
Like a URL route mapped to a controller: the route (path) is the stable public contract; the file backing it (directory) can be moved without changing the route.
saying these in an interview costs you the question
- Claiming you must rename the path or change dependencies when you relocate a project's directory.
- Overriding projectDir/buildFileName casually without documenting it, harming team discoverability.