skip to content

You're migrating an old build that uses tasks.create everywhere to tasks.register. What changes in behavior must you watch for, and why isn't it always a drop-in replacement?

level: seniorimportance: should knowfreq 35%

answer

  1. Task vs TaskProvider return type
  2. side effects now lazy or never run
  3. must delaze getByName/all/withType too
  4. move side effects to plugin apply
  5. verify with --scan / config cache

basics

~20 s

create returns a Task, register returns a TaskProvider — callers expecting a concrete Task break. Code that relied on the task existing/configured immediately (side effects, ordering, getByName) may now run later or not at all unless realized.

solid answer

~50 s

`register` isn't a pure drop-in for `create` because it changes *when* and *whether* configuration runs. Three things bite: (1) **Return type** — `create` returns a `Task`/typed instance, `register` returns `TaskProvider<T>`; any caller doing `create(...).someProperty` must switch to `provider.configure {}`, `map`, or move logic into the block. (2) **Timing of side effects** — if a `create` block had side effects (registering extensions, mutating other tasks, eager `dependsOn` to a concrete task), those now run lazily or never; ordering assumptions can break. (3) **Forced realization elsewhere** — other code using `getByName`/`all`/iteration will still realize the task, so you only get the win if you also delaze the surrounding wiring. Practically: register the task, replace `getByName` with `named`, `all`/`withType {}` with `configureEach`, and read outputs via `map`/`flatMap`. Then verify with a build scan that `gradle help` no longer realizes unrelated tasks. Also confirm configuration-cache compatibility, which `register` patterns support better.

code

kotlin · 8 lines
kotlin
// before: eager, direct property access, side effect at config time
val jar = tasks.create("jar", Jar::class)
jar.archiveFileName.set("app.jar")
project.extensions.add("built", true)   // ran every build

// after: lazy; side effect moved OUT of any task block
val jar = tasks.register("jar", Jar::class) { archiveFileName.set("app.jar") }
project.extensions.add("built", true)   // keep in plugin apply, not in a lazy block

go deeper

for a junior

Recognize that the return type changes from Task to TaskProvider and that you can't just swap the call.

for a middle

Explain the three pitfalls (return type, deferred side effects, surrounding eager APIs) and the lazy replacements.

for a senior

Drive a whole-graph migration: relocate side effects to apply, delaze wiring, and verify with build scans and config-time profiling.

for a principal

Tie the migration to configuration-cache adoption and build-scalability goals; sequence it across many modules and enforce lazy patterns in shared plugins.

## Why it's not mechanical `tasks.create` does two things eagerly: returns a usable `Task` *and* runs configuration now. `tasks.register` does neither up front. Swapping them changes the program's observable behavior in subtle ways. ## 1. Return type change ```kotlin // before val jar: Jar = tasks.create("jar", Jar::class) jar.archiveFileName.set("app.jar") // direct property access // after val jar: TaskProvider<Jar> = tasks.register("jar", Jar::class) jar.configure { archiveFileName.set("app.jar") } // must configure lazily ``` Any downstream code that treated the result as a concrete `Task` must change to `configure {}`, `map`, or `flatMap`. ## 2. Side effects move (or vanish) If a legacy `create` block did things like: ```kotlin tasks.create("setup") { project.extensions.add("foo", Foo()) // side effect at config time } ``` with `register`, that side effect runs **only when `setup` is realized** — which may be never for a build that doesn't run `setup`. Side effects belong in the plugin's `apply`, not in a (now-lazy) task block. Hidden ordering dependencies ("task A's block configured task B") similarly break. ## 3. Realization leaks defeat the migration Replacing `create` with `register` buys nothing if `tasks.getByName("x")`, `tasks.all { }`, `withType(Foo) { }`, or container iteration elsewhere still realizes the task. Migration is a *whole-graph* change: | Replace | With | |---|---| | `create("x")` | `register("x")` | | `getByName("x")` | `named("x")` | | `all { }` | `configureEach { }` | | `withType(T) { }` | `withType(T).configureEach { }` | | `t.output` (concrete) | `provider.flatMap { it.output }` | ## 4. Verify the win ```bash ./gradlew help --scan # inspect task-creation timeline ./gradlew help --profile # configuration time before/after ``` If `gradle help` realizes tasks unrelated to help, a leak remains. ## 5. Bonus: configuration cache Lazy `register`/provider patterns are friendlier to the **configuration cache** (which serializes the task graph). Eager `create` plus concrete-instance capture often triggers config-cache problems, so the migration usually pays off twice. ## Checklist - Switch return-type usages to provider APIs. - Move config-time side effects out of task blocks. - Delaze surrounding wiring (named/configureEach/flatMap). - Re-scan to confirm fewer realizations and stable config time.

  • A legacy create block configured another task as a side effect. Why is that dangerous after migrating to register?
    Because the block now runs only when the task is realized; if the build never realizes it, the other task is never configured, silently changing behavior. Such cross-task setup should live in plugin apply, not a lazy task block.
  • How does migrating to register help the configuration cache?
    Lazy registration and provider wiring avoid capturing concrete Task/Project state at configuration time, which is exactly what the configuration cache forbids serializing; eager create patterns more often trip cache incompatibilities.
  • Why might the migration show no speedup?
    Because eager APIs elsewhere (getByName, all, withType {}, iteration) still realize the registered tasks; you must delaze the whole wiring for the configuration-avoidance benefit to appear.

saying these in an interview costs you the question

  • Treating register as a textual find-replace for create.
  • Leaving config-time side effects inside now-lazy task blocks.
  • Declaring victory without re-checking realizations via a build scan.

context