The Three Kotlin Versions in a Gradle Project

A Kotlin project built with Gradle doesn't have one Kotlin version, it has three. Here is how to tell them apart.

Table of Contents

Introduction

Do you know what version of Kotlin your Gradle build is using?

There are three Kotlin versions you need to know about in a project built with Gradle. They’re easy to mix up, and mixing them up can lead to some confusion (and the occasional compiler error).

1. The Kotlin that compiles your code #

This is the version most people look for. If your application or library code is written in Kotlin and gets compiled by the Kotlin Gradle Plugin, you pick its version in your build:

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.3.21"
}

Change the KGP version, and you change the Kotlin your project is written in. This is the version you control directly.

2. The Kotlin embedded in Gradle #

Gradle ships its own Kotlin compiler and standard library inside the distribution. It’s the compiler that builds your Kotlin DSL and build logic, and its standard library is on the classpath your build scripts and the plugins in your build run against.

You never declare this one. It comes bundled with whatever Gradle version you use. In Gradle 9.7.0:

# gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip

The embedded Kotlin version is 2.4.0, declared in Gradle’s own version catalog:

# distribution.versions.toml
[versions]
kotlin = "2.4.0!!"

[libraries]
kotlinStdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }

If you can’t remember which version is embedded, you can consult the Kotlin compatibility section of the Gradle User Manual:

Embedded Kotlin

3. The Kotlin language version for your build logic #

Here’s the sneaky one. Even though Gradle embeds a particular compiler, it compiles your build logic against a fixed language version.

You can think of the Kotlin language version as a setting handed to the embedded compiler at compile time.

Since the language version is tied to your Gradle version, the same build logic can compile on one Gradle release and fail on another. Say you’re on Gradle 8.10, which targets language version 1.8, and you reach for a data object (added in Kotlin 1.9) in a build script:

// build.gradle.kts on Gradle 8.10, language version 1.8
sealed interface Stage
data object Build : Stage   // won't compile: 'data object' needs language version 1.9+
data object Test : Stage

It won’t compile. Upgrade the wrapper to Gradle 9.0.0, which targets language version 2.2, and the exact same code now works:

// build.gradle.kts on Gradle 9.0.0, language version 2.2
sealed interface Stage
data object Build : Stage   // 'data object' is available
data object Test : Stage

The Kotlin language version is usually older than the embedded version, and that’s deliberate:

Language Version

Pinning the language version is Gradle’s documented backward-compatibility policy: backward-incompatible Kotlin upgrades only happen at major Gradle releases.

That’s why the script language version held at 1.8 through the entire Gradle 8.x line and only moved to 2.2 at Gradle 9.0.0. It keeps existing build scripts and plugins compiling even as the embedded compiler underneath them gets updated.

And this works safely because Kotlin builds for it. The compiler’s -language-version and -api-version flags officially support at least the three previous language and API versions alongside the latest stable one, so a 2.4.0 compiler holding your scripts to 2.2 is squarely inside Kotlin’s own supported window.

Let’s recap #

So there are three version numbers, but only two actual compilers.

#1, the Kotlin Gradle Plugin, is a genuinely separate compiler you download and choose. It compiles your application code, and it’s the one version you control directly.

#2 and #3 are not two different compilers. #2 is the compiler and standard library Gradle bundles (the tool), while #3 is the language version that tool is told to target when it compiles your build logic (a setting on the tool).

Put them together and in Gradle 9.7.0, your build.gradle.kts is compiled by the embedded 2.4.0 compiler, at language level 2.2.

# Version What it does What sets it
1 Kotlin Gradle Plugin Compiles your application code Your build logic
2 Embedded Kotlin The Kotlin runtime and compiler Gradle bundles for build logic Your Gradle version
3 Language version The syntax level #2 is told to target for your build logic Your Gradle version

The catch #

The version you pick in plugins { ... } controls how your application code is compiled. But anything that runs inside the build executes on Gradle’s runtime, right alongside the embedded Kotlin. That includes your buildSrc, your precompiled script plugins, and even the Kotlin Gradle Plugin itself. That shared runtime means the Kotlin your build logic uses can’t drift too far from the version Gradle embeds.

If you let them drift too far apart, things start to break. The classic symptom is a warning about multiple versions of Kotlin on the classpath, which shows up when buildSrc or a plugin pulls in a Kotlin far from the embedded one. (The Kotlin Gradle Plugin didn’t respect Gradle’s runtime cleanly until version 1.5.10; see KT-41142.)

There aren’t hard numbers for “too far,” because it depends on which parts of the Kotlin standard library your build logic actually touches, and that compatibility boundary is owned by the Kotlin team, not Gradle.

So the practical rule is this: your application’s Kotlin version can move on its own, but the Kotlin you use in your build logic should stay close to whatever your Gradle version embeds.

For the background on how Kotlin manages this compatibility, see the Kotlin evolution principles.

For a wider tour of the friction this causes, see @eskatos’s issue on mixing Kotlin versions in Gradle builds.

Why it matters #

Two things are worth taking away from all of this.

First, the separation is by design. The embedded Kotlin and the Kotlin you pick do different jobs. The embedded version powers Gradle’s own engine and the Kotlin DSL, while the version in plugins { ... } decides how your application code is compiled.

While you can pick the latter, there is a limit. Because your build logic runs on the embedded Kotlin’s runtime, the version you pick and the version Gradle ships can’t wander too far apart. Check the Kotlin compatibility section of the Gradle User Manual when you need to know exactly which versions line up.

Second, bumping Gradle is rarely just bumping Gradle. A new wrapper version usually moves all three Kotlin versions at once, and each one can ask something of you:

  1. Your build logic may need touch-ups for the new Kotlin language version, since deprecated syntax in build.gradle.kts and settings.gradle.kts can stop compiling.

  2. Your Kotlin Gradle Plugin (KGP) version likely needs a bump too, if your project applies the Kotlin plugin. KGP is tested and supported against specific Gradle versions, so bumping Gradle usually means bumping KGP.

  3. Your Kotlin source code can be affected as well, because bumping KGP pulls in a newer Kotlin compiler, and that newer compiler can surface fresh deprecations (or even compilation errors) in your actual application code.

Discuss