skip to content

When would you replace a project(':lib') dependency in the same build with a composite build (includeBuild), and how does ordering change?

level: seniorimportance: nice to knowfreq 30%

answer

  1. project(':lib') = same build/subproject
  2. includeBuild = separate build grafted in
  3. automatic dependency substitution
  4. ordering spans builds, same DAG semantics
  5. great for cross-repo local dev

basics

~20 s

Use a composite build when :lib lives in its own separate Gradle build/repo. includeBuild substitutes a normal module:lib dependency with the local build, so Gradle still orders producer before consumer — but across builds, not subprojects.

solid answer

~40 s

`project(':lib')` works only inside one Gradle build (one `settings.gradle` tree). When :lib is a *separate* build — its own repo or independently published library — a composite build via `includeBuild("../lib")` lets the consuming build **substitute** the external coordinates (`com.acme:lib`) with the local source build. You keep declaring `implementation("com.acme:lib:1.0")`, and Gradle dependency-substitutes it to the included build's project, wiring the same producer→consumer task ordering across build boundaries: the included build's `jar` runs before the consumer compiles. This is ideal for cross-repo local development without publishing snapshots. Trade-offs: composite builds add coordination overhead, don't share a single configuration phase as tightly, and some plugins/lifecycle assumptions differ. Within a single multi-project build, plain `project(':lib')` is simpler and faster. The ordering *semantics* (artifact-driven, DAG-based) are the same; the *boundary* moves from subproject to whole build.

code

kotlin · 8 lines
kotlin
// app build: settings.gradle.kts
rootProject.name = "app"
includeBuild("../lib")   // ../lib is its own Gradle build producing com.acme:lib

// app/build.gradle.kts -- unchanged declaration:
dependencies { implementation("com.acme:lib:1.0") }
// Gradle substitutes com.acme:lib with the local included build;
// ../lib:jar runs before :app:compileJava.

go deeper

for a junior

Know project(':lib') is for same-build subprojects; composite builds (includeBuild) graft a separate build in.

for a middle

Explain automatic dependency substitution and that ordering still puts the included build's jar first.

for a senior

Weigh subproject vs composite by repo/ownership boundaries and dev-loop needs; describe substitution semantics and overhead.

for a principal

Decide monorepo-subproject vs multi-repo-composite topology at org scale, governing substitution, versioning, and CI implications.

## Two ways to depend on local code - **Subproject dependency**: `implementation(project(':lib'))` — :lib is a subproject of the *same* build, listed via `include(":lib")` in `settings.gradle`. - **Composite (included) build**: `includeBuild("../lib")` in `settings.gradle` — :lib is its *own* standalone Gradle build, possibly a different repo, that you graft in. ## Why composite builds exist You publish `com.acme:lib` as a library. Your app declares: ```kotlin dependencies { implementation("com.acme:lib:1.0") } ``` Normally that resolves from a repository. During local development you want to edit :lib *and* :app together without publishing snapshots. Add: ```kotlin // settings.gradle.kts (app build) includeBuild("../lib") ``` Gradle performs **automatic dependency substitution**: it sees the included build produces `com.acme:lib`, and substitutes the binary dependency with the *local source build*. No build-file change to the dependency declaration is needed. ## Ordering across builds The substitution carries the same **artifact-driven ordering**: resolving :app's `compileClasspath` now points at the included build's `:lib` outgoing artifact, so the included build's `jar`/`compileJava` run before :app compiles. The producer→consumer DAG simply spans two builds. You can confirm with `:app:dependencies` — the node shows `project :lib` (from the included build) instead of `com.acme:lib:1.0`. ## When to choose which | Situation | Choose | |---|---| | Modules co-located, one repo, one settings tree | `project(':lib')` | | :lib is a separately published/owned/repo'd build | composite `includeBuild` | | Patch a third-party-style lib locally to test a fix | composite `includeBuild` | | Tight, fast, single-config build | `project(':lib')` | ## Trade-offs of composites - Slightly heavier: each included build has its own settings/lifecycle. - Some plugin or task-path assumptions that work intra-build need care across builds. - Great for cross-repo dev loops and for replacing published snapshots. ## Manual substitution You can also substitute explicitly: ```kotlin includeBuild("../lib") { dependencySubstitution { substitute(module("com.acme:lib")).using(project(":")) } } ``` Useful when coordinates don't auto-match. ## Bottom line Same ordering semantics, different boundary. Reach for composites when the producer is a *separate build*; stay with `project(':lib')` when it's a *subproject*.

  • Do you change the dependency declaration when adding includeBuild?
    Usually no. Gradle automatically substitutes the published coordinates (com.acme:lib) with the included build's matching output. You only add includeBuild in settings; explicit dependencySubstitution is needed only when coordinates don't auto-match.
  • Does ordering still place :lib before :app with a composite build?
    Yes. After substitution the consumer's classpath resolves to the included build's artifact, so its jar/compileJava run before the consumer compiles — same producer→consumer DAG, just spanning two builds.
  • Why not always use composite builds?
    Within a single repo, project(':lib') is simpler and faster: one settings tree, one configuration phase, tighter task wiring. Composites add per-build lifecycle overhead and are best reserved for genuinely separate builds/repos.

saying these in an interview costs you the question

  • Saying project(':lib') can reference a project in a different Gradle build.
  • Claiming includeBuild requires rewriting every dependency to project(...) (substitution is automatic).
  • Asserting composite builds lose producer→consumer ordering.

context