When :app declares implementation(project(':lib')), how does Gradle decide the order in which :lib and :app tasks run?
answer
- project(':lib') = artifact dependency
- compileClasspath needs :lib apiElements/jar
- implicit task dep, no dependsOn
- topological sort of a DAG
- :app:dependencies shows project :lib
basics
~10 sBecause :app's compile classpath needs :lib's jar, Gradle automatically builds :lib first. The project dependency creates implicit task dependencies, so :lib:jar runs before :app:compileJava — you don't wire that ordering by hand.
solid answer
~40 sA `project(':lib')` dependency isn't just metadata — it ties :app's compile classpath to artifacts :lib produces. Resolving :app's `compileClasspath` configuration requires :lib's `apiElements`/`jar` outputs, so Gradle adds an implicit task dependency: `:lib:jar` (and its upstream `:lib:compileJava`, `:lib:classes`) must complete before `:app:compileJava`. This is *artifact-driven ordering*: tasks depend on each other because one consumes another's output, not because you called `dependsOn`. The whole thing forms a directed acyclic graph (DAG) that Gradle topologically sorts. Run `:app:dependencies --configuration compileClasspath` to see the `project :lib` edge, and `:app:compileJava` will show `:lib:jar` as a prerequisite. The benefit: declare *what* you need, and ordering falls out automatically and correctly, even across many projects.
code
kotlin · 7 lines// app/build.gradle.kts
dependencies {
implementation(project(":lib"))
}
// No dependsOn needed: resolving app's compileClasspath
// pulls :lib's apiElements -> :lib:jar runs before :app:compileJava.
// Inspect: ./gradlew :app:dependencies --configuration compileClasspathgo deeper
Know that project(':lib') makes Gradle build :lib first automatically; you don't write dependsOn.
Explain it as artifact-driven: compileClasspath consumes :lib's apiElements/jar, which induces the implicit task dependency, and tasks form a DAG.
Connect to configurations (resolvable vs consumable), variant selection (apiElements), and how the graph stays minimal so only the affected slice rebuilds.
Frame ordering as data-flow modeling across a large module graph; discuss how clean artifact contracts keep the DAG sound and enable caching/incrementality at org scale.
## The problem: ordering across projects In a multi-project build, `:app` depends on `:lib` for code. You never want to manually say "build :lib before :app" — that's brittle. Gradle derives the order from declared dependencies. ## Project dependencies are artifact dependencies When you write: ```kotlin dependencies { implementation(project(":lib")) } ``` you are saying: *:app's `implementation` bucket includes whatever `:lib` produces for consumption*. `implementation` feeds the `compileClasspath` and `runtimeClasspath` resolvable configurations. To resolve `compileClasspath`, Gradle must obtain :lib's compile-facing artifact — its `apiElements` variant, which carries the jar (or, with `java-library`, sometimes classes directories). ## How ordering is induced Resolving a configuration that references another project's *outgoing artifacts* makes the consuming task **depend on the producing task**. So: - `:app:compileJava` reads `compileClasspath` → needs `:lib`'s `apiElements` → needs `:lib:jar` → needs `:lib:classes` → needs `:lib:compileJava`. Gradle inserts these as **implicit task dependencies**. No `dependsOn` is written by you. This is the key idea: tasks are wired by **data flow** (inputs/outputs), not manual ordering. Configurations expose artifacts via `outgoing`, and a `ProjectDependency` carries a *built-by* relationship that Gradle honours. ## The graph (DAG) All these implicit and explicit task dependencies form a **directed acyclic graph**. At execution Gradle topologically sorts it, so every producer runs before its consumers. "Acyclic" matters — a cyclic project dependency (`:a`→`:b`→`:a`) is an error. ## Inspecting it ```bash ./gradlew :app:dependencies --configuration compileClasspath ``` shows a `project :lib` node. To see the *task* graph, `--dry-run` lists tasks in execution order; `:lib:jar` precedes `:app:compileJava`. ## Why it matters You declare intent (`project(':lib')`), and correct, minimal ordering is computed for you — across two projects or two hundred. Combined with the build cache and up-to-date checks, only the affected slice rebuilds.
- Did you write a dependsOn to make :lib build first?No. The implementation(project(':lib')) dependency makes :app's compileClasspath consume :lib's outgoing artifact, which Gradle translates into an implicit task dependency on :lib:jar. Manual dependsOn would be redundant and worse.
- Which task of :app first triggers building :lib?:app:compileJava, because it resolves compileClasspath. (test compilation/jar/run can trigger it too, but compileJava is the earliest in a typical build.)
Like a kitchen ticket system: the plating station doesn't schedule the grill — it just orders a steak, and the grill must finish before plating starts. Declaring the ingredient implies the order.
saying these in an interview costs you the question
- Saying you must add dependsOn(':lib:jar') by hand to order multi-project builds.
- Claiming project dependencies are resolved at configuration time and force :lib to fully build during configuration (resolution/ordering is at execution).