skip to content

Project Dependencies project(':lib')

Declaring implementation(project(':lib')) or api(project(':lib')) and how that choice controls what downstream consumers can see. Asked because reaching for api everywhere is the most common multi-project mistake.

on this pageshow

questions

5

In a Gradle multi-project build, how do you make one subproject depend on another subproject so it can use its classes and outputs?

level: juniorimportance: must knowfreq 80%

answer

  1. project(":lib") notation
  2. no publishing — consumes build outputs
  3. auto task ordering
  4. configuration controls classpath
  5. include(...) in settings file

basics

~10 s

Add a project dependency in the consumer's build file, e.g. implementation(project(":lib")). Gradle builds :lib first and puts its compiled output and dependencies on the consumer's classpath.

solid answer

~30 s

Inside a multi-project build (subprojects listed in `settings.gradle.kts` via `include(":lib", ":app")`), one subproject depends on another by declaring a **project dependency** rather than an external coordinate: ```kotlin // app/build.gradle.kts dependencies { implementation(project(":lib")) } ``` The `project(":lib")` notation resolves to the sibling project. Gradle automatically wires task ordering so `:lib` is assembled before `:app` compiles, and `:lib`'s main output (its JAR / classes) plus its declared dependencies are placed on `:app`'s classpath according to the configuration used (`implementation`, `api`, `testImplementation`, etc.). You don't publish `:lib` to a repository — Gradle consumes its outputs directly from the build.

code

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

// app/build.gradle.kts
dependencies {
    implementation(project(":lib"))
}

go deeper

for a junior

Recall the project(":lib") syntax and that it puts the other module's code on your classpath without publishing.

for a middle

Explain task-ordering is automatic and that the configuration (implementation/api) governs classpath and transitivity.

for a senior

Mention variant selection (apiElements/runtimeElements) and how attribute matching picks the right outgoing configuration of the java component.

for a principal

Frame project dependencies as the substrate for module decomposition; discuss when to keep modules in one build vs split into separate published artifacts / composite builds.

## What a project dependency is A Gradle build can contain many **subprojects** (modules). They are declared in the settings file: ```kotlin // settings.gradle.kts rootProject.name = "my-build" include(":lib", ":app") ``` When one subproject needs another's code, you declare a **project dependency** using the `project(path)` notation instead of an external coordinate like `com.example:lib:1.0`. ```kotlin // app/build.gradle.kts dependencies { implementation(project(":lib")) } ``` ## How it differs from an external dependency - An **external dependency** (`implementation("group:name:version")`) is resolved from a repository (Maven Central, etc.). - A **project dependency** is resolved from the **build itself** — Gradle uses `:lib`'s produced artifacts (by default the main JAR / classes directory) without publishing anything. ## What Gradle does for you 1. **Task ordering / dependency graph** — `:app`'s `compileJava`/`compileKotlin` automatically depends on `:lib` being built (its `jar` or `classes`), so you never have to wire `dependsOn` manually. 2. **Classpath assembly** — `:lib`'s output and its *exported* dependencies are added to `:app`'s classpath, governed by the configuration (`implementation` vs `api`). 3. **Variant selection** — Gradle picks the right output **variant** of `:lib` (e.g. the `apiElements` / `runtimeElements` configuration of the `java` component) using attribute matching, so the consumer gets the correct compile vs runtime artifacts. ## The configuration matters The configuration you put the project dependency in controls visibility and transitivity: - `implementation(project(":lib"))` — `:lib` is on `:app`'s compile and runtime classpath, but is **not** leaked to anyone who depends on `:app`. - `api(project(":lib"))` — requires the `java-library` plugin; `:lib` is also exposed to `:app`'s consumers. - `testImplementation(project(":lib"))` — only for `:app`'s tests. ## Path notation Paths are colon-separated and rooted at the build: `:lib`, `:platform:core`, etc. The leading `:` means "from the root". A typo in the path fails fast at configuration time with "project ... not found".

  • Do you have to publish :lib to a Maven repository for :app to use it?
    No. A project dependency consumes :lib's outputs directly from the build; nothing is published. Gradle builds :lib first and uses its produced artifacts and exported dependencies on :app's classpath.
  • Does :app automatically build :lib first?
    Yes. The project dependency creates an edge in the task graph, so :app's compile/jar tasks depend on :lib's relevant tasks; Gradle orders them correctly without any manual dependsOn.

It's like importing code from a folder in your own repo instead of pulling a published library off the internet — same import syntax for you, but Gradle compiles the dependency locally first.

saying these in an interview costs you the question

  • Saying you must publish the subproject to a repository first.
  • Manually wiring dependsOn between compile tasks instead of using a project dependency.
  • Using a file path like project("../lib") instead of the colon path project(":lib").

context

open as a page

When wiring one subproject onto another with project(...), what is the difference between declaring it with api(project(':lib')) versus implementation(project(':lib'))?

level: middleimportance: must knowfreq 75%

basics

~10 s

implementation(project(":lib")) keeps :lib private to the consumer — downstream projects can't see it. api(project(":lib")) exposes :lib transitively to anyone depending on the consumer. api needs the java-library plugin.

open as a page

When you declare implementation(project(":lib")), what exactly does :app consume from :lib, and how does Gradle choose it?

level: seniorimportance: should knowfreq 40%

basics

~10 s

By default Gradle consumes :lib's main java component — its produced JAR (or classes/resources for compile) plus its exported dependencies — selecting the correct outgoing configuration via attribute matching, not a hardcoded JAR path.

open as a page

Walk through how api vs implementation on project dependencies controls what a downstream consumer can see across a chain of subprojects (:app → :service → :core).

level: seniorimportance: should knowfreq 55%

basics

~20 s

Compile-time visibility flows only through api edges. If :service declares api(project(":core")), then :app (depending on :service) can compile against :core. If it's implementation, :core is hidden from :app at compile time but still present at runtime.

open as a page

How would you decide when to model a piece of code as a separate subproject wired in via project(...) dependencies versus keeping it in an existing module?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Split into a separate subproject when the code has a clear, reusable boundary, needs an enforced API surface, or improves parallel/incremental builds. Wire it with implementation(project(...)) by default, exposing only its real public API via api.

open as a page