Setting up Kotlin: Gradle, the K2 compiler and project layout
Install a JDK, structure a Gradle build, configure the Kotlin plugin and compiler options, and run, test and package from the command line.
A minimal Gradle build
// build.gradle.kts
plugins {
kotlin("jvm") version "2.1.0"
application
}
group = "com.example"
version = "0.1.0"
repositories { mavenCentral() }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
}
kotlin {
jvmToolchain(21)
compilerOptions {
languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1)
// K2 is the default compiler since 2.0; opt out only to work around a bug
freeCompilerArgs.add("-Xjsr305=strict")
}
}
application { mainClass.set("com.example.MainKt") }
tasks.test { useJUnitPlatform() }gradle wrapper --gradle-version 8.11 # commit the wrapper, not the binary
./gradlew run
./gradlew test
./gradlew build --scan # a build scan explains where time goes
./gradlew installDist # runnable script plus jars in build/install- Always commit the Gradle wrapper. It pins the build tool version so every machine and CI runner behaves the same.
- Use
jvmToolchain(21)so the build provisions a matching JDK rather than relying on whatever happens to be on the PATH. - Declare dependency versions in a version catalog (
gradle/libs.versions.toml) once you have more than one module. - Enable the build cache and configuration cache in
gradle.properties:org.gradle.caching=trueandorg.gradle.configuration-cache=true.
Source sets and where files go
| Path | Contains | Compiled into |
|---|---|---|
src/main/kotlin | Production Kotlin | The main artefact |
src/main/resources | Config and data files | Bundled on the classpath |
src/test/kotlin | Unit tests | A separate source set |
src/integrationTest/kotlin | Slow tests | A custom source set |
gradle/libs.versions.toml | Dependency catalogue | Not compiled |
The package declaration must match the directory from src/main/kotlin downwards. Kotlin does not enforce it the way Java does, but a mismatch makes navigation and generated code confusing.
Splitting into modules
// settings.gradle.kts
rootProject.name = "service"
include(":core", ":app", ":adapters:postgres")
// app/build.gradle.kts
dependencies {
implementation(project(":core"))
runtimeOnly(project(":adapters:postgres"))
}⚠️
Use
implementation rather than api for internal dependencies. An api dependency leaks onto every consumer's compile classpath, so changing it forces the whole graph to recompile.FAQ
Do I need the Kotlin plugin and the Java plugin?
The
kotlin("jvm") plugin applies the Java plugin for you. Add a separate java block or jvmToolchain only to configure the toolchain and source compatibility.Why does the build recompile everything after a small change?
Usually an
api dependency changed, a Gradle plugin forced a rebuild of the task inputs, or the build cache is disabled. Run with --info or a build scan to see which task was invalidated.Related
Testing Kotlin with JUnit 5, Kotest and MockK Java interop, JVM tooling and build performance
Last refreshed 2026-09-18.