A teammate wired a project dependency `project(':lib')` but the other module lives in a separate Gradle build. Why does this fail, and what's the correct mechanism?
answer
- project(':lib') = same build only
- separate build → unknown project
- includeBuild = composite build
- dependency substitution on coordinates
- keep the normal coordinate, not project()
basics
~10 sproject(':lib') only resolves subprojects included in the same build's settings.gradle(.kts). A module in a separate build isn't a subproject, so the path is unknown. Use includeBuild (a composite build) to substitute it.
solid answer
~40 s`project(":path")` references a **subproject of the current build** — one declared via `include(...)` in this build's `settings` file. If the target lives in a *different* Gradle build (its own settings, own root), it isn't part of this build's project hierarchy, so `:lib` resolves to nothing and the build fails. The right mechanism is a **composite build**: add `includeBuild("../lib-build")` in `settings.gradle(.kts)`. Gradle then **substitutes** any external dependency on `lib`'s published coordinates with the local build's output automatically, wiring tasks across the two builds. So in the consumer you keep declaring the *normal coordinate* (`implementation("com.acme:lib:1.0")`), and `includeBuild` redirects it to the local project — no `project(':lib')`. This is how you develop across separately-versioned repos without publishing.
code
kotlin · 8 lines// consumer/settings.gradle.kts
includeBuild("../lib-build")
// consumer/build.gradle.kts
dependencies {
// ordinary coordinate; includeBuild substitutes the local build output
implementation("com.acme:lib:1.0")
}go deeper
Know project(':lib') only references modules in the same build.
Identify that a separate build needs includeBuild, not a project dependency.
Explain dependency substitution, keeping the coordinate, and explicit substitution rules.
Design cross-repo dev workflows: composite builds for local iteration vs. published artifacts in CI/prod.
## Why the project dependency fails A Gradle **build** has exactly one `settings.gradle(.kts)` defining its **project hierarchy** via `include(...)`. The `project(":lib")` notation can only name a project in *that* hierarchy. If `:lib` is actually the root of a *separate* build (different repo, its own settings file), it's invisible here — the path doesn't exist, and resolution fails with an unknown-project error. ## The correct mechanism: composite builds Gradle's **composite build** feature lets one build include another *whole* build: ```kotlin // consumer settings.gradle.kts includeBuild("../lib-build") ``` Now the two builds are coordinated. The key behavior is **dependency substitution**: when the consumer declares a normal module dependency that matches an artifact the included build produces, Gradle transparently replaces the external dependency with the included build's local output and adds the necessary task dependencies. ```kotlin // consumer build.gradle.kts — keep the ordinary coordinate dependencies { implementation("com.acme:lib:1.0") // substituted by includeBuild output } ``` You do **not** switch to `project(":lib")` — that's specifically wrong here. Optionally you can declare explicit substitutions if coordinates don't line up: ```kotlin includeBuild("../lib-build") { dependencySubstitution { substitute(module("com.acme:lib")).using(project(":")) } } ``` ## project dependency vs. includeBuild — the contrast - **`project(":lib")`** — same build, sibling subproject, one settings file, one project graph. - **`includeBuild("...")`** — *separate* builds composed together; each keeps its own settings/versioning; wiring happens via dependency substitution on coordinates. ## Why it matters Composite builds let teams develop a library and its consumer **simultaneously across repos** without publish/install round-trips, while production still resolves the library from a repository by coordinate. It's the bridge between in-build modularization (project deps) and fully published artifacts.
- In a composite build, do you change `implementation("com.acme:lib:1.0")` to `project(":lib")`?No. You keep the normal coordinate; `includeBuild` performs dependency substitution to redirect it to the local build's output.
- What if the included build's coordinates don't match the declared dependency?Use an explicit `dependencySubstitution { substitute(module(...)).using(project(...)) }` block inside `includeBuild`.
- How does this differ from buildSrc?buildSrc is an implicitly-included build for shared build logic on the classpath; `includeBuild` composes arbitrary external builds and substitutes their published artifacts during development.
project(':lib') is asking for a colleague by desk number inside your office; includeBuild is partnering with another company — you still address them by their public name (coordinate), and the partnership reroutes the request to their local team.
saying these in an interview costs you the question
- Saying `project(':lib')` works across separate builds.
- Switching to `project()` notation under includeBuild instead of keeping the coordinate.
- Confusing composite builds with multi-project (subproject) builds.