skip to content

Build Lifecycle Phases

The three build phases — initialization, configuration, execution — the task graph computed between them, and the cost of configuring every project. The single most important Gradle concept in interviews.

on this pageshow

explore

questions

27

During which build phase does the cost of eager task configuration occur, and how does the Configuration Avoidance API reduce that cost?

level: juniorimportance: must knowfreq 70%

answer

  1. configuration phase runs every build
  2. tasks.register defers configuration action
  3. TaskProvider, realized only when needed
  4. unused tasks never configured
  5. saving is config-phase, not execution

basics

~10 s

The configuration phase runs every build and configures all tasks. Configuration Avoidance (tasks.register) defers a task's configuration until something actually needs it, so unused tasks are never configured.

solid answer

~40 s

Gradle runs three phases each invocation: initialization, configuration, then execution. The configuration phase always runs for the whole build and builds the task graph — historically `tasks.create` eagerly configured every task here, even tasks not part of the requested work. The Configuration Avoidance API (`tasks.register`) returns a `TaskProvider` and defers the configuration action until the task is *realized* — i.e. actually needed for the requested graph. So for `./gradlew compileJava`, test/javadoc/etc. tasks registered lazily are never configured, cutting configuration-phase time. The key insight: this is a *configuration-phase* optimization — execution is unaffected. It pays off most in large multi-project builds where eager configuration of hundreds of unused tasks dominated wall-clock time before the build even started running.

code

kotlin · 10 lines
kotlin
// Eager: configures during configuration phase even if never run
tasks.create("slowReport") { doLast { generate() } }

// Lazy: configuration block runs only if slowReport enters the task graph
val slowReport = tasks.register("slowReport") {
    doLast { generate() }
}

// named() defers extra configuration of an existing task too
tasks.named("jar") { archiveBaseName.set("app") }

go deeper

for a junior

Know the three phases in order and that tasks.register defers configuration so unused tasks aren't configured.

for a middle

Explain that configuration runs every build for the whole project graph, so deferring unused tasks' configuration is a real startup saving.

for a senior

Tie it to TaskProvider/realization, note .get() defeats it, and distinguish it from the Configuration Cache.

for a principal

Frame it as a build-startup performance lever across a large multi-project codebase and a prerequisite discipline for adopting the Configuration Cache.

## The three lifecycle phases Every Gradle invocation runs three phases in order: 1. **Initialization** — Gradle decides which projects participate (reads `settings.gradle(.kts)`) and creates a `Project` object for each. 2. **Configuration** — Gradle executes the build scripts of *all* participating projects to build the **task graph** (a DAG of `Task` objects and their dependencies). This phase runs **every build**, regardless of which task you asked for. 3. **Execution** — Gradle runs the subset of tasks needed for the requested task(s), respecting up-to-date checks and the build cache. The critical fact: **configuration runs for the whole build every time**, even `./gradlew help`. So any work done while *defining* tasks is paid on every invocation. ## The eager problem The legacy `tasks.create("foo") { ... }` API both **creates** and **configures** the task immediately during the configuration phase. In a build with hundreds of tasks (plus those contributed by plugins), this meant configuring every task — resolving its inputs/outputs, running its configuration closures — even though a typical command only executes a handful. This is wasted configuration-phase work. ## Configuration Avoidance The Configuration Avoidance API replaces eager creation with **lazy registration**: - `tasks.register("foo", Foo::class) { ... }` returns a `TaskProvider<Foo>` and does **not** run the configuration action immediately. - `tasks.named("bar") { ... }` looks up an existing task lazily, also returning a `TaskProvider` and deferring the extra configuration. - The task is only **realized** (created and configured) when something forces it — being part of the requested task graph, or a call that needs the concrete `Task` (e.g. `.get()`). If a registered task is never needed for the requested work, its configuration action never runs. That is the saving — measured in the configuration phase. ```kotlin // Lazy: configuration block runs only if 'report' ends up in the graph val report = tasks.register<Report>("report") { description = "Generates the report" outputDir.set(layout.buildDirectory.dir("reports")) } ``` ## Why it interacts with the phase model Because the saving is purely a configuration-phase concern, it shows up as faster *startup* before any task runs. Pair it with **lazy `Provider`/`Property`** values so even the wiring between tasks (inputs/outputs) is computed lazily, and avoid `.get()` during configuration — calling it forces realization and defeats avoidance. Note: the Configuration Cache is a separate, complementary feature that *stores* the configured task graph to skip the configuration phase entirely on subsequent runs; configuration avoidance reduces the cost *within* a configuration phase that still runs.

  • Does configuration avoidance speed up the execution phase?
    No — execution runs the same tasks either way. The saving is entirely in the configuration phase, by skipping configuration of tasks that aren't part of the requested graph.
  • What does it mean for a registered task to be 'realized'?
    Realization is when Gradle actually creates the Task instance and runs its deferred configuration action — triggered by the task entering the requested graph or by code that needs the concrete Task (e.g. .get()).

Eager creation is cooking every dish on the menu before anyone orders; configuration avoidance is prepping a dish only once it's actually ordered.

saying these in an interview costs you the question

  • Claiming the configuration phase only runs for the tasks you requested (it runs for the whole build, every time).
  • Saying configuration avoidance speeds up execution rather than the configuration phase.

context

open as a page

What happens during Gradle's configuration phase, and what is its output?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Gradle evaluates every project's build script top to bottom, running the build-script code to register and configure tasks. The output is the task model — the set of tasks Gradle knows about and how they depend on each other.

open as a page

What happens during Gradle's Execution phase, and how does it differ from the Configuration phase?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Execution is the third (last) phase. Gradle runs the @TaskAction methods of the selected tasks in dependency order. Configuration, the earlier phase, only builds task objects and wires their dependencies — it does no actual work.

open as a page

What happens during Gradle's initialization phase, and how does it differ from the configuration phase?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Initialization runs first: Gradle evaluates settings.gradle(.kts) to learn which projects exist (via include), then creates a Project object for each. Configuration runs next, executing each project's build.gradle to register tasks.

open as a page

What is the task execution graph in Gradle, and at what point in the build lifecycle is it constructed?

level: juniorimportance: must knowfreq 70%

basics

~10 s

It's a directed acyclic graph (DAG) of the tasks Gradle will run for the requested build, ordered by their dependencies. Gradle builds it after the configuration phase finishes and before execution starts.

open as a page

What operations during the configuration phase force a lazily-registered task to be realized, defeating configuration avoidance?

level: middleimportance: must knowfreq 60%

basics

~10 s

Calling .get() on a TaskProvider, or using eager APIs like tasks.getByName, tasks.all, or withType {} without a lazy variant, forces the task to be created and configured during the configuration phase.

open as a page

How does choosing `tasks.register` over `tasks.create` affect the configuration phase?

level: middleimportance: must knowfreq 65%

basics

~20 s

tasks.create eagerly builds and configures the task during the configuration phase, even if it's never run. tasks.register defers that work until the task is actually needed, so unused tasks cost almost nothing at configuration time.

open as a page

How do you tell whether a piece of build-script code runs at configuration time or execution time, and why does it matter?

level: middleimportance: must knowfreq 60%

basics

~20 s

Code directly in the script body or in a task's configuration block runs at configuration time, on every build. Code inside doFirst/doLast/@TaskAction runs at execution time, only when the task runs. It matters because configuration-time work slows every command.

open as a page

How does Gradle decide which tasks to execute and in what order during the Execution phase?

level: middleimportance: must knowfreq 65%

basics

~20 s

Gradle starts from the tasks you named on the command line, pulls in every task they depend on (dependsOn), then topologically sorts the whole set. It runs them in an order that respects dependencies and mustRunAfter/shouldRunAfter ordering rules.

open as a page

How does Gradle decide a task is UP-TO-DATE during execution, and what makes it re-run?

level: middleimportance: must knowfreq 60%

basics

~20 s

Before running a task, Gradle snapshots its declared inputs and outputs. If both match the previous run (and outputs still exist), it skips the action and reports UP-TO-DATE. Any change to an input, output, or the task's code makes it re-run.

open as a page

What is the difference between include and includeBuild in settings.gradle.kts?

level: middleimportance: must knowfreq 60%

basics

~10 s

include adds a subproject to the current build (one shared build hierarchy). includeBuild wires in a separate, standalone Gradle build as a composite build, substituting its outputs for matching external dependencies.

open as a page

How do dependsOn, finalizedBy, mustRunAfter, and shouldRunAfter each shape the task graph, and how do they differ?

level: middleimportance: must knowfreq 65%

basics

~20 s

dependsOn adds a real edge that pulls the dependency into the graph. finalizedBy schedules a task to run after another, even on failure. mustRunAfter/shouldRunAfter only constrain ordering — they don't add the task to the graph.

open as a page

How do Provider<T> and Property<T> support configuration avoidance during the configuration phase, and why is calling .get() too early a problem?

level: middleimportance: should knowfreq 50%

basics

~10 s

Provider/Property hold values lazily — the value is computed only when queried. Wiring tasks with providers (via map/flatMap) instead of calling .get() during configuration keeps work deferred and can avoid realizing producer tasks.

open as a page

Why does Gradle configure every project on every build by default, and what problems does that cause?

level: middleimportance: should knowfreq 50%

basics

~20 s

By default Gradle evaluates all participating projects' build scripts so it has a complete, consistent task model and can resolve cross-project dependencies. The cost is that even unrelated subprojects are configured on every command, slowing builds in large repos.

open as a page

What happens when a task fails during execution, and how does `--continue` change Gradle's behavior?

level: middleimportance: should knowfreq 45%

basics

~20 s

By default Gradle stops at the first task failure (fail-fast): no further tasks that depend on it run, and the build exits non-zero. With --continue, Gradle keeps running every task whose dependencies still succeeded, then reports all failures together at the end.

open as a page

How does Gradle build the project hierarchy from include() declarations, and how do nested project paths map to directories?

level: middleimportance: should knowfreq 35%

basics

~10 s

include(":web:api") creates intermediate projects (:web and :web:api), each a Project node under rootProject. By default a project path maps to a matching directory; you can override the location with project(":api").projectDir.

open as a page

What is the Settings object, and what kinds of build-wide configuration belong in settings.gradle.kts rather than in a build script?

level: middleimportance: should knowfreq 45%

basics

~10 s

The Settings object is the receiver of settings.gradle.kts. It declares build structure (include/includeBuild, rootProject.name) and build-wide concerns: pluginManagement repositories, dependencyResolutionManagement, and version catalogs — things that must apply before any build script runs.

open as a page

How does declaring a task's inputs and outputs create implicit dependency edges in the task graph?

level: middleimportance: should knowfreq 50%

basics

~10 s

If task B consumes a file produced as task A's declared output, Gradle infers that B depends on A and adds that edge automatically — you don't need an explicit dependsOn.

open as a page

When you run `gradle build`, how does Gradle decide which tasks end up in the execution graph, and how are duplicates handled?

level: middleimportance: should knowfreq 45%

basics

~10 s

Gradle starts from the tasks you named (the requested tasks), walks their dependencies transitively, adds any finalizers, and deduplicates so each task appears once. The result is the ordered execution graph.

open as a page

A large multi-project build has a slow configuration phase. How would you diagnose and reduce configuration-phase time using configuration avoidance?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Profile with --profile or a build scan to see configuration time, find eager task creation/realization (create, getByName, all, withType{}), convert to register/named/configureEach, and wire by provider. Consider the Configuration Cache.

open as a page

What are the behavioral differences between tasks.create and tasks.register with respect to the configuration phase, and what subtle bugs can surface when migrating?

level: seniorimportance: should knowfreq 40%

basics

~20 s

create configures the task immediately during configuration; register defers it until realized. Migrating can surface ordering bugs: code that relied on a task existing/being configured eagerly (e.g. getByName right after) may now find it unrealized.

open as a page

A team reports that even `gradle help` is slow in their large monorepo. How would you diagnose and reduce configuration-phase time?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Since help runs no real tasks, slowness is configuration-phase cost. Profile with --profile/build scans, find eager create, script-body I/O and .get() calls, switch to lazy register/Provider APIs, and enable the configuration cache.

open as a page

Explain the different task outcomes Gradle reports during execution (UP-TO-DATE, SKIPPED, NO-SOURCE, FROM-CACHE) and what triggers each.

level: seniorimportance: should knowfreq 40%

basics

~20 s

No label = the action ran. UP-TO-DATE = inputs/outputs unchanged. FROM-CACHE = outputs pulled from the build cache. SKIPPED = an onlyIf {} predicate was false (or the task was excluded). NO-SOURCE = the task's declared source inputs were empty.

open as a page

Walk through what Gradle does internally when it executes a single task — from the task graph to the @TaskAction.

level: seniorimportance: should knowfreq 30%

basics

~20 s

For each task in graph order, Gradle resolves its inputs, runs up-to-date/cache checks, and if work is needed runs the task's action list — doFirst actions, then @TaskAction methods, then doLast actions — capturing outputs and recording an outcome.

open as a page

When does an init.gradle (initialization) script run relative to settings.gradle, and what is each responsible for?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Init scripts run first, before settings.gradle is evaluated, against a Gradle object — used for machine/CI-wide setup (mirrors, credentials, global plugins). settings.gradle runs next, against the Settings object, defining the build's project structure.

open as a page

Gradle fails graph construction with a circular dependency error. What does this mean, and how do you diagnose and resolve it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

It means the dependency edges form a cycle, so no valid execution order exists. Read the printed cycle path, then break it — usually by replacing a wrong dependsOn with an ordering hint or by extracting shared work into a third task.

open as a page

Why does Gradle compute the entire task graph before executing any task, and what capabilities does this 'plan-then-run' model enable?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Knowing the whole graph up front lets Gradle schedule independent tasks in parallel, decide up-to-date/cached status correctly, fail fast on cycles, and report progress — because it sees all the work and its ordering before running anything.

open as a page