skip to content

When does apply(from = ...) execute relative to the rest of the build script, and what subtle issues does that cause?

level: seniorimportance: should knowfreq 30%

answer

  1. eager + synchronous + configuration-time
  2. executes inline at the apply line
  3. only later code sees its effects
  4. last-writer-wins ordering bugs
  5. apply early; prefer lazy Provider/named

basics

~20 s

apply(from = ...) runs synchronously at configuration time, at the exact point it appears in the script. Statements after it see its effects; statements before it don't. Ordering bugs arise when a script plugin depends on, or is depended on by, later lines.

solid answer

~50 s

`apply(from = ...)` is an **eager, synchronous configuration-time** call: the referenced script is read and executed **inline at the line where apply appears**. So anything the script plugin sets up (tasks, extensions, properties) is visible only to code that runs **after** the apply call; code above it won't see it. This causes subtle bugs: applying a script plugin **after** you've already configured something it also touches can overwrite or be overwritten depending on order; relying on a value the script plugin defines **before** applying it gives a missing/default value. Because it runs at configuration time, it also can't be made lazy or conditional on task execution. The defensive patterns: apply script plugins **early** (near the top), avoid order-dependent mutation, prefer `Provider`/`Property` and `tasks.named { }` lazy configuration so ordering matters less, and for cross-cutting setup graduate to a plugin whose `apply()` runs deterministically.

code

kotlin · 8 lines
kotlin
// Order matters — apply executes inline:
println(tasks.findByName("hello")) // null
apply(from = "defines-hello.gradle.kts")
println(tasks.findByName("hello")) // task ':hello'

// Reading a value defined by the script plugin: apply FIRST
apply(from = "versions.gradle.kts")
val springVersion: String by extra // safe only after apply

go deeper

for a junior

Know it runs at configuration time, in place, so later lines see its effects and earlier ones don't.

for a middle

Explain last-writer-wins ordering bugs and the read-before-apply pitfall.

for a senior

Recommend defensive ordering plus lazy Provider/named APIs, and articulate cross-project allprojects/afterEvaluate interactions.

for a principal

Push order-sensitive cross-cutting setup into precompiled plugins with deterministic apply() entry points instead of positional apply(from=) calls.

## Configuration-time, inline, eager Gradle builds run in phases: **initialization** (settings), **configuration** (build scripts run, task graph built), **execution** (tasks run). `apply(from = ...)` happens during **configuration**, and it is **eager**: the moment the interpreter reaches that line, it stops, reads the target script, executes its entire body against the apply target, then resumes the outer script. That means evaluation order is purely **textual/positional**: ```kotlin // build.gradle.kts val before = tasks.findByName("hello") // null — not defined yet apply(from = "defines-hello.gradle.kts") // registers task "hello" val after = tasks.findByName("hello") // now found ``` ## Where it bites **1. Order-dependent mutation.** If both your build script and the script plugin configure the same extension/task, the last writer wins. Move the apply line and behavior changes — a classic heisenbug. **2. Reading before applying.** A `versions.gradle.kts` that sets `extra["springVersion"]` is useless if you read `extra["springVersion"]` *above* the apply call. **3. No laziness / conditionality at execution time.** apply runs during configuration; you can't defer it to task execution or skip it based on a task being requested. (You *can* wrap it in a normal `if`, but that's still configuration-time.) **4. Cross-project surprises.** Applying inside `allprojects { apply(from = ...) }` runs the script once per project, in project-evaluation order — interacting with `evaluationDependsOn` and afterEvaluate semantics. ## Defensive patterns ```kotlin // 1) Apply script plugins early, before dependent config apply(from = "versions.gradle.kts") // 2) Prefer lazy config so textual order matters less tasks.named("jar") { /* configured lazily, regardless of apply position */ } // 3) Use Provider/Property instead of reading raw values eagerly val springVersion: String by extra ``` - Keep `apply(from = ...)` near the top of the script. - Avoid having both the script plugin and the build script mutate the same thing. - Lean on lazy APIs (`tasks.named`, `Provider`) so you're wiring providers, not reading snapshots. - For anything order-sensitive and cross-cutting, a precompiled plugin gives a single, well-defined `apply()` entry point and is easier to reason about than positional `apply(from=)` calls scattered through scripts. ## Mental model Think of `apply(from = ...)` as a **textual include executed in place** — not a deferred import. Its effects are scoped to everything textually after it within the same evaluation.

  • Is apply(from = ...) lazy or eager?
    Eager and synchronous — it executes the target script immediately, inline, during the configuration phase, at the textual position of the apply call.
  • How can lazy APIs reduce ordering sensitivity around script plugins?
    Using tasks.named { } and Provider/Property wires configuration that's resolved later, so textual position of mutations matters less than with eager findByName/get-and-set patterns.

saying these in an interview costs you the question

  • Saying apply(from=) is deferred/lazy like an import.
  • Claiming apply order never matters.
  • Suggesting it can run at task-execution time.

context