skip to content

Kotlin Task Configuration API

Configuring tasks lazily in Kotlin with register, named, and withType().configureEach instead of eagerly with getByName. Interviewers ask to check that you know task configuration avoidance, not just the syntax.

on this pageshow

questions

5

In the Gradle Kotlin DSL, what is the difference between tasks.register<Jar>("x") and tasks.create<Jar>("x"), and which should you prefer?

level: juniorimportance: must knowfreq 70%

answer

  1. register = lazy TaskProvider
  2. create = eager realized instance
  3. configuration avoidance
  4. configuration-phase cost
  5. prefer register everywhere

basics

~10 s

register() is lazy — the task is only configured if it ends up in the build. create() is eager — it builds and configures the task immediately. Prefer register().

solid answer

~30 s

`tasks.register<Jar>("myJar")` returns a `TaskProvider<Jar>` and defers both creation and configuration until the task is actually needed (queried or scheduled to run). `tasks.create<Jar>("myJar")` returns the realized `Jar` task instance and runs its configuration block immediately during the configuration phase, regardless of whether the task runs. With large builds, eager creation forces every task object to be instantiated and configured on every invocation — even `./gradlew help` — which is the classic configuration-time hot spot. `register` is part of Gradle's *configuration avoidance* API. Always prefer `register` for new tasks; reserve `create` for legacy plugins that genuinely need the instance up front.

code

kotlin · 10 lines
kotlin
// Lazy — preferred
val zip = tasks.register<Zip>("packageDist") {
    archiveFileName.set("dist.zip")
    from(layout.buildDirectory.dir("output"))
}

// Eager — avoid
val zip2 = tasks.create<Zip>("packageDistEager") {
    archiveFileName.set("dist.zip")
}

go deeper

for a junior

Know register is lazy and preferred, create is eager. State the one-line difference.

for a middle

Explain the configuration phase, TaskProvider, and when the block actually runs.

for a senior

Tie it to configuration-time performance on large multi-project builds and the full configuration-avoidance family (named/withType/configureEach).

for a principal

Set a team convention banning eager APIs, discuss build-scan configuration-time metrics, and migrating legacy plugins off create/getByName.

## The configuration phase problem Every Gradle build runs in phases: **initialization → configuration → execution**. During *configuration*, Gradle evaluates your build scripts to build the task graph. Historically, defining a task with `task myJar(type: Jar)` (Groovy) or `tasks.create<Jar>("myJar")` (Kotlin) *realized* the task immediately: it allocated the object and ran its configuration closure right then — even for tasks that would never run in this invocation. In a build with thousands of tasks this is wasteful: running `./gradlew help` still pays to configure your packaging, signing, and publishing tasks. ## Configuration avoidance Gradle introduced the **configuration-avoidance API** to fix this. The key entry points in the Kotlin DSL: - `tasks.register<T>("name") { ... }` — returns a `TaskProvider<T>`. The task is **not created** and the block **not executed** until something *realizes* the provider (it's scheduled to run, or `.get()`/an accessor forces it). - `tasks.named<T>("name") { ... }` — also returns a `TaskProvider<T>`; configures an existing registered task lazily. Contrast with the **eager** API: - `tasks.create<T>("name") { ... }` — returns the realized `T`, configured immediately. - `tasks.getByName("name")` — realizes and returns the task immediately. ## TaskProvider `register` hands back a `TaskProvider<T>`, a lazy reference. You can wire it into other tasks' inputs without realizing it, e.g. another task can depend on the provider directly. Only when the dependency is actually needed does Gradle create the underlying task. ```kotlin val myJar = tasks.register<Jar>("myJar") { archiveBaseName.set("app") from(sourceSets.main.get().output) } // no Jar instance exists yet; configuring assemble below does NOT realize myJar tasks.named("assemble") { dependsOn(myJar) } ``` ## Rule of thumb Use `register` for every task you define. Avoid `create`, `getByName`, `getByType`, and `tasks.all { }` (eager) unless a third-party plugin forces your hand. Prefer `named` over `getByName` and `withType<T>().configureEach { }` over `withType<T> { }`.

  • What type does register() return and why does that matter?
    A TaskProvider<T> — a lazy handle. It lets you wire dependencies and inputs without forcing the task to be created, preserving configuration avoidance.
  • If you only ever run `help`, does a registered Jar task get configured?
    No. Its configuration block runs only if the task is realized — queried via .get()/an accessor, or scheduled to run. `help` realizes neither, so the block never executes.

register is like adding a contact's name to your phone; create is calling the person immediately to introduce yourself even if you never need them.

saying these in an interview costs you the question

  • Saying create() and register() are interchangeable with no performance difference.
  • Claiming register() runs the configuration block immediately.

context

open as a page

Why should you prefer tasks.named<Test>("test") { } over tasks.getByName("test") { } when configuring an existing task in the Kotlin DSL?

level: middleimportance: must knowfreq 60%

basics

~10 s

named() configures the task lazily via a TaskProvider, so it's only realized if needed. getByName() realizes the task immediately, defeating configuration avoidance.

open as a page

What does the tasks { } block do in the Kotlin DSL, and what is the eager-realization trap when you reference a task by accessor like tasks.test inside it?

level: middleimportance: should knowfreq 35%

basics

~10 s

tasks { } gives you the TaskContainer as receiver so you can call register/named without the tasks. prefix. Referencing a typed accessor like tasks.test realizes that task eagerly.

open as a page

How do you wire one task to depend on or consume the output of another in the Kotlin DSL while preserving configuration avoidance?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Keep references as TaskProviders from register/named and pass the provider (or its flatMap output) to dependsOn or an input property. Avoid calling .get(), which realizes the task.

open as a page

Explain tasks.withType<Test>().configureEach { } in the Kotlin DSL. How does it differ from tasks.withType<Test> { } and tasks.withType<Test>().all { }?

level: seniorimportance: should knowfreq 45%

basics

~10 s

configureEach applies a configuration lazily to all current and future tasks of a type, realizing each only when needed. withType { } and .all { } apply eagerly, realizing every matching task now.

open as a page