No history yet

Gradle Build Lifecycle

The Three Phases of a Build

Every time you run a Gradle command, like gradle build, you kick off a structured, three-phase process. This isn't just a random sequence of events; it's a deliberate lifecycle designed for efficiency and scalability. Understanding these phases—Initialization, Configuration, and Execution—is the key to writing fast, maintainable builds and avoiding common performance traps.

Lesson image

Initialization: Finding the Projects

Before Gradle can build anything, it needs to know what it's building. The Initialization phase is all about discovery. Gradle scans for a settings.gradle.kts file to determine which projects are part of the build. This file acts as the single source of truth for your project structure, whether you have a single project or a complex multi-project build.

// settings.gradle.kts

rootProject.name = "my-awesome-app"

// Defines the projects that are part of the build
include(":app", ":core-library")

During this phase, Gradle creates a Settings object. You can use this object within settings.gradle.kts to programmatically configure which projects to include. For example, you could read a properties file to conditionally include certain modules, which is useful for large-scale projects.

Configuration: Building the Plan

Once Gradle knows which projects are involved, the Configuration phase begins. In this phase, Gradle executes the build.gradle.kts script for each project. It's important to understand that it executes the entire script from top to bottom. The primary goal here isn't to run tasks, but to evaluate your build logic and construct a complete model of all the tasks and their relationships.

This model is known as the (DAG). Each task is a node in the graph, and dependencies between tasks are the edges. For example, the jar task depends on the compileJava task, which in turn might depend on processResources. Gradle builds this graph to create an efficient execution plan, ensuring tasks run in the correct order and only when needed.

This is where a critical performance pitfall lies. Since the entire build script is executed during configuration, any heavy operation—like reading a large file, making a network call, or performing complex calculations—will slow down every single build run, regardless of which task you actually intend to execute. This is called a and is a common source of slow builds.

Anti-Pattern: Avoid doing real work during the Configuration phase. This code reads a file every time Gradle configures the project.

// build.gradle.kts

tasks.register("myTask") {
    // BAD: This file is read during configuration, not execution.
    val fileContent = File("large-data-file.json").readText()

    doLast {
        // The work should happen here, during the execution phase.
        println("File content has ${fileContent.length} characters.")
    }
}

The correct approach is to defer work to the Execution phase by placing logic inside a task's action, such as doFirst or doLast.

Execution: Running the Tasks

The Execution phase is where the magic happens. After building the DAG, Gradle determines the specific tasks you requested and all of their dependencies. It then executes them in the correct order.

Thanks to and task output caching, Gradle is smart about this. If a task's inputs (like source files) haven't changed since the last run, and its outputs (like compiled class files) are still present, Gradle will mark the task as UP-TO-DATE and skip it entirely. This is why subsequent builds are often much faster than the first one.

By understanding these three phases, you can write build scripts that are not only correct but also efficient. Keeping the configuration phase lean and letting the execution phase handle the heavy lifting ensures your builds remain fast and responsive as your project grows.