From Groovy to Kotlin DSL: Migrating Gradle Build Scripts
Back to Blog

From Groovy to Kotlin DSL: Migrating Gradle Build Scripts

6 min read
Read in Deutsch

From Groovy to Kotlin DSL: Migrating Gradle Build Scripts

We have been using Gradle as the build tool for our JVM projects since 2018 -- starting with the switch from Maven to Gradle, later explored further in our article on Groovy as a DSL language in Gradle. In parallel, Kotlin has evolved over the years into our preferred language for Spring Boot backends, as we described in our posts on Kotlin in the Spring ecosystem and the practical experience report. Nevertheless, we kept our build scripts in Groovy for a long time. We have now changed that.

Why Switch at All?

Groovy in Gradle works. We have proven that over the years. But there is a problem that becomes increasingly apparent with growing projects: the IDE does not know what is happening in a build.gradle. Groovy's dynamic typing, which we described as a strength in our Groovy article, becomes a weakness when editing build scripts. Auto-completion works at best rudimentarily, refactoring support is practically nonexistent, and errors only surface at runtime -- meaning during the next gradle build.

The Kotlin DSL solves exactly this problem. Build scripts are statically typed, IntelliJ IDEA knows every type, every method, every property. You get real auto-completion, can navigate into the Gradle API via Cmd+Click, and see errors immediately as red underlines. For teams that already write Kotlin, there is an additional benefit: you have a unified language for production code and build logic.

With Gradle 7.x, the Kotlin DSL has matured. The performance issues of earlier versions have been resolved, IDE integration in IntelliJ IDEA works reliably, and the entire Gradle documentation provides examples in both variants. There is no longer a good reason to stay with Groovy when building Kotlin projects.

The Migration Strategy

Gradle allows mixing Groovy and Kotlin DSL scripts within a single build. This is the key to a low-risk migration. You do not have to convert everything at once but can proceed file by file.

Our order:

  1. Migrate settings.gradle to settings.gradle.kts
  2. Convert the root build.gradle to build.gradle.kts
  3. Migrate subproject build scripts individually

The step is the same each time: rename the file, adjust the syntax, run the build, test. Thanks to the ability to mix scripts, you can deploy after each step without breaking anything.

The Most Important Syntax Differences

Strings: Double Instead of Single Quotes

In Groovy, both single and double quotes are allowed for strings. In Kotlin, only double quotes exist. This affects practically every line in the build script.

Groovy:

group = 'com.encircle360'
version = '1.0.0-SNAPSHOT'

Kotlin DSL:

group = "com.encircle360"
version = "1.0.0-SNAPSHOT"

Plugins Block

The plugins block changes syntactically because Kotlin requires parentheses for method calls and does not allow implicit method calls without them.

Groovy:

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'org.jetbrains.kotlin.jvm' version '1.6.21'
    id 'org.jetbrains.kotlin.plugin.spring' version '1.6.21'
}

Kotlin DSL:

plugins {
    java
    id("org.springframework.boot") version "2.7.0"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    id("org.jetbrains.kotlin.jvm") version "1.6.21"
    id("org.jetbrains.kotlin.plugin.spring") version "1.6.21"
}

For standard plugins like java, the Kotlin DSL provides accessor properties that can be used directly without id(). This is shorter and type-safe.

Assignments with the = Operator

In Groovy, you can assign properties with a space -- which is actually a method call. In Kotlin, the assignment operator = must be used.

Groovy:

sourceCompatibility = '17'
targetCompatibility = '17'

springBoot {
    mainClass = 'com.encircle360.app.Application'
}

Kotlin DSL:

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

springBoot {
    mainClass.set("com.encircle360.app.Application")
}

This shows another advantage of the Kotlin DSL: the assignment of sourceCompatibility uses the JavaVersion enum instead of a string. Errors like sourceCompatibility = '1.17' are caught immediately.

Dependencies

For dependencies, parentheses and double quotes become mandatory. Additionally, the Groovy shorthand of omitting parentheses in method calls is no longer available.

Groovy:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
    implementation 'org.jetbrains.kotlin:kotlin-reflect'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Kotlin DSL:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    runtimeOnly("org.postgresql:postgresql")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

The difference may seem cosmetic but has a tangible benefit: IntelliJ recognizes the dependency coordinates as strings in a method call and can apply code inspections to them.

Custom Tasks

Task definitions change the most noticeably. The Groovy syntax with task taskName(type: TaskType) no longer exists. Instead, you use tasks.register<TaskType>("taskName").

Groovy:

task cleanBuildDir(type: Delete) {
    delete 'build/generated'
}

task printVersion {
    doLast {
        println "Version: ${project.version}"
    }
}

Kotlin DSL:

tasks.register<Delete>("cleanBuildDir") {
    delete("build/generated")
}

tasks.register("printVersion") {
    doLast {
        println("Version: ${project.version}")
    }
}

The generic type specification <Delete> immediately makes clear which task type is being used. In the IDE, this gives you auto-completion for all properties and methods of the respective task type.

Extra Properties Instead of ext

In Groovy, the ext block is commonly used to define project-wide variables. The Kotlin DSL provides the extra mechanism for this purpose.

Groovy:

ext {
    springCloudVersion = '2021.0.3'
    kotlinVersion = '1.6.21'
}

Kotlin DSL:

val springCloudVersion by extra("2021.0.3")
val kotlinVersion by extra("1.6.21")

In subprojects, you access these with val springCloudVersion: String by rootProject.extra. This is somewhat more verbose than in Groovy, but type-safe -- the variable has a concrete type, and the compiler verifies it.

Pitfalls in Practice

Buildscript block: If you still use a buildscript block for older plugin repositories, you need to apply the same syntax changes there as well. Better yet, take the opportunity to replace the buildscript block entirely with the plugins block combined with pluginManagement in settings.gradle.kts.

Groovy plugins: Some older Gradle plugins only expose their configuration as Groovy extensions. In rare cases, the configuration does not work directly in the Kotlin DSL, and you need to fall back to withGroovyBuilder or the<ExtensionType>(). With current plugins like Spring Boot, Kotlin, or Flyway, we had no issues.

String interpolation: Groovy uses ${variable} only in double-quoted strings; in single-quoted strings, variables are not resolved. Since Kotlin always uses double quotes, $variable and ${expression} always work. But be careful: if you need $ as a literal character, you must escape it.

Performance on first build: After renaming to .gradle.kts, the first build takes a bit longer because Gradle compiles the script for the first time. After that, the script cache kicks in, and build times are comparable to Groovy.

Our Conclusion

We performed the migration in an existing multi-module project with six subprojects. In total, the conversion took about half a day, spread across multiple merge requests. The biggest time investment is not in the syntax conversion itself -- that is mechanical -- but in testing whether all build variants, profiles, and CI pipelines continue to work.

The result was worth it. IDE support is noticeably better, errors in build scripts are caught earlier, and new team members who know Kotlin find their way around immediately. For projects that already use Kotlin as their development language, the migration is a clear recommendation. The effort is manageable, and the day-to-day benefits are lasting.

Patrick Hütter

Written by

Patrick Hütter

Founder & Software Architect

Software architect, engineer and entrepreneur. Patrick has been building products and platforms for over a decade — from enterprise backends and cloud-native infrastructure to AI-powered applications. As founder of encircle360, he combines deep technical expertise with entrepreneurial vision, driving open source projects that create real impact.