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=true and org.gradle.configuration-cache=true.

Source sets and where files go

PathContainsCompiled into
src/main/kotlinProduction KotlinThe main artefact
src/main/resourcesConfig and data filesBundled on the classpath
src/test/kotlinUnit testsA separate source set
src/integrationTest/kotlinSlow testsA custom source set
gradle/libs.versions.tomlDependency catalogueNot 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.

Testing Kotlin with JUnit 5, Kotest and MockK Java interop, JVM tooling and build performance

Last refreshed 2026-09-18.