skip to content

How do you declare a dependency on another module within the same Gradle multi-project build, and how does it differ from depending on an external library?

level: juniorimportance: must knowfreq 70%

answer

  1. project(':lib') not a coordinate
  2. logical path from settings, not filesystem
  3. implicit task dependency — builds first
  4. consumes apiElements/runtimeElements
  5. no publish, incremental

basics

~10 s

Use project(':lib') instead of a coordinate string, e.g. implementation(project(':lib')). Gradle wires the local subproject directly rather than resolving it from a remote repository.

solid answer

~30 s

Within a multi-project build you declare a **project dependency** with the `project(':path')` notation: `implementation(project(':lib'))`. The colon-separated path is the logical project path (from `settings.gradle(.kts)`), not a filesystem path. Unlike an external dependency — declared as a `group:name:version` coordinate that Gradle resolves from a repository — a project dependency points at a sibling subproject in the same build. Gradle builds that subproject first (it becomes a task dependency), then consumes its produced artifact and its transitive dependencies through the producer's `apiElements`/`runtimeElements` outgoing configurations. This gives you incremental, source-aware builds: changing `:lib` rebuilds and re-tests dependents without publishing anything.

code

kotlin · 8 lines
kotlin
// settings.gradle.kts
include(":app", ":lib")

// app/build.gradle.kts
dependencies {
    implementation(project(":lib"))                 // local subproject
    implementation("com.google.guava:guava:33.0.0-jre") // external module
}

go deeper

for a junior

Know the project(':lib') notation and that it points at a sibling module, not a downloaded artifact.

for a middle

Explain the implicit task dependency, transitive api propagation, and the path-is-logical-not-filesystem distinction.

for a senior

Discuss variant-aware wiring via apiElements/runtimeElements and how conflicts resolve in one graph.

for a principal

Frame when to keep code in-build as project deps vs. extracting to published libraries / composite builds for org-scale modularization.

## What a project dependency is A Gradle build can contain many **subprojects** (also called modules or projects), each declared in `settings.gradle(.kts)` via `include(":app", ":lib")`. The leading colon and colons separating segments form a **logical project path** (`:lib`, `:libs:core`), independent of where the directory actually lives on disk. When one subproject needs another, you declare a **project dependency**: ```kotlin dependencies { implementation(project(":lib")) } ``` ## How it differs from an external dependency - **External / module dependency**: `implementation("com.google.guava:guava:33.0.0-jre")` — a `group:name:version` coordinate that Gradle **resolves** from a declared repository (Maven Central, etc.), downloading a published artifact and its POM/module metadata. - **Project dependency**: `project(":lib")` — points at a subproject in the *same build*. Nothing is downloaded; Gradle adds an implicit **task dependency** so `:lib` is assembled before its consumer, then wires the consumer's classpath to `:lib`'s outgoing artifacts. ## What gets wired under the hood Gradle uses **variant-aware** resolution. The producer (`:lib`) exposes *consumable* configurations such as `apiElements` (compile classpath) and `runtimeElements` (runtime classpath). The consumer's *resolvable* configurations (`compileClasspath`, `runtimeClasspath`) select the matching variant by attributes (e.g. `Usage`, `LibraryElements`). So `implementation(project(":lib"))` transitively brings in whatever `:lib` declared as `api` dependencies and the appropriate classes/jar. ## Practical consequences - **Incremental builds**: editing `:lib` triggers recompilation of dependents — no publish step. - **Up-to-date checks & build cache** work across the boundary. - The dependency participates in the same **dependency graph**, so version conflicts between `:lib`'s transitive deps and yours are resolved together. Use project dependencies for code you control inside the build; use coordinates for third-party or already-published artifacts.

  • Is `:lib` a filesystem path?
    No — it's a logical project path derived from `settings.gradle(.kts)`. The directory can be remapped with `project(":lib").projectDir = file("...")`.
  • Does Gradle publish `:lib` to a repository to make this work?
    No. It wires the consumer directly to the producer's outgoing artifacts in-build and adds a task dependency so `:lib` is built first.

An external dependency is ordering a part from a catalog; a project dependency is grabbing a part your own factory builds on the next bench — no shipping, and if you change the blueprint it's remade on the spot.

saying these in an interview costs you the question

  • Claiming `project(':lib')` is a path on disk.
  • Saying it requires publishing to mavenLocal first.
  • Confusing it with `includeBuild` (that's composite builds across separate builds).

context