In a large monorepo, what's your strategy for using projectDir customization, and what governance concerns does decoupling logical paths from disk layout raise?
answer
- override = indirection cost
- one rule via helper, not exceptions
- settings = single source of truth
- default-first, deviate deliberately
- flattening loses path aggregation
basics
~10 sUse projectDir sparingly and consistently — e.g. a single documented rule like 'flat paths, folders grouped under services/'. Avoid ad-hoc per-project overrides because they make the build hard to navigate and reason about.
solid answer
~40 sDecoupling logical paths from disk is powerful but introduces **indirection**: a reader of `settings.gradle.kts` must consult the `projectDir` map to know where `:auth` actually lives. At scale I treat it as a deliberate convention, not a per-project escape hatch: - **One rule, applied uniformly** (e.g. flat path, folder under `services/<name>`) via a loop, so the mapping is mechanical and predictable. - **Keep settings.gradle.kts the single source of truth** for the path↔directory map; don't scatter overrides. - **Prefer the default** (`include(":lib")` → `./lib`) whenever there's no concrete benefit — convention beats configuration for onboarding and tooling. - **Avoid `buildFileName`** (deprecated); standardise on per-directory `build.gradle.kts` so IDEs and scripts stay predictable. Governance: enforce the convention in review, document it, and watch that path-based aggregation expectations still hold once paths are flattened.
code
kotlin · 7 lines// settings.gradle.kts — convention encoded once
fun service(name: String) {
include(":$name")
project(":$name").projectDir = file("services/$name")
}
listOf("auth", "billing", "catalog").forEach(::service)
// Predictable: every :<name> lives at services/<name>go deeper
Out of depth; at most know overrides exist and defaults are simplest.
Recognise the indirection cost and prefer defaults unless there's a reason.
Propose a single encoded convention and keep settings the source of truth.
Own the org-wide layout policy: weigh indirection vs flexibility, enforce in review, manage migration of path-based aggregation, and document deviations.
## The core tension: indirection vs. flexibility The default mapping (path → directory) is **self-documenting**: see `:services:auth`, find `services/auth`. Every `projectDir` override breaks that self-documentation, replacing it with a lookup in `settings.gradle.kts`. For a handful of projects this is harmless; across hundreds, undisciplined overrides turn settings into an opaque routing table. Good governance means the indirection is **systematic and minimal**, not ad-hoc. ## A scalable strategy 1. **Pick one structural convention and encode it as code, not exceptions.** For example: flat logical paths, directories grouped by domain. ```kotlin // settings.gradle.kts fun service(name: String) { include(":$name") project(":$name").projectDir = file("services/$name") } listOf("auth", "billing", "catalog").forEach(::service) ``` A reader learns one rule and can predict every location. Compare this to fifty bespoke `project(":x").projectDir = file("...")` lines, each a special case. 2. **Single source of truth.** Keep the entire path↔directory map in `settings.gradle.kts`. Don't let build scripts assume locations; they should reference projects by logical path only. 3. **Default-first.** Only deviate from `include(":x")` → `./x` when there's a concrete payoff (domain grouping, migration, externally-shared directory). Each deviation is cognitive cost; pay it deliberately. 4. **Avoid deprecated knobs.** `buildFileName` is deprecated in modern Gradle; mandating conventional `build.gradle.kts` keeps IDE navigation, code search, and CI tooling predictable. ## Governance concerns - **Discoverability/onboarding**: new engineers expect path-to-folder correspondence. Document any deviation prominently in the repo README and `settings.gradle.kts` comments. - **Lost path-based aggregation**: flattening `:services:*` to `:auth` removes the ability to address the group by path prefix; if your CI relies on `:services:build`-style aggregation, replace it with explicit lifecycle tasks or plugin-based grouping before flattening. - **Tooling assumptions**: some scripts/CI infer module roots from paths; an override map can surprise them. Centralising in settings limits the blast radius. - **Refactor safety**: because logical paths are stable independent of directories, you can move folders without touching dependency references (`project(":auth")`), which is a genuine refactoring advantage — provided the override map is updated atomically. - **Consistency enforcement**: encode the convention in a helper function (as above) and enforce in code review so no one adds a one-off exception. ## When NOT to decouple If your team has no compelling grouping or migration need, leave path and directory coupled. The most maintainable monorepo is often the most boring one — conventional layout, conventional build-file names, minimal indirection.
- What's the main downside of many ad-hoc projectDir overrides at scale?They destroy the self-documenting path→directory correspondence, turning settings.gradle.kts into an opaque routing table that hurts onboarding and tooling that infers locations from paths.
- If you flatten :services:auth to :auth, what should you check first?Whether anything depends on path-based group aggregation of :services. Replace it with explicit lifecycle tasks or plugin-based grouping before removing the path hierarchy.
- Why does stable logical paths make folder refactors safer?Dependency references use the logical path (project(":auth")), so moving the folder only updates the projectDir map in settings — no need to touch every reference across the build.
Like DNS for your build: a few well-managed CNAME records add flexibility, but a sprawl of hand-edited overrides becomes an unmaintainable routing table no one trusts.
saying these in an interview costs you the question
- Recommending heavy per-project projectDir use without a unifying convention
- Promoting buildFileName as a structuring tool despite its deprecation
- Ignoring that flattening paths removes path-based aggregation