Gradle and packaging
The Kotlin ecosystem revolves around Gradle — the conventional build system for Kotlin projects. The Gradle Kotlin DSL (build.gradle.kts) admits substantial Kotlin-style configuration; alternatives are the older Groovy DSL (build.gradle) and Maven. Kotlin Multiplatform admits cross-platform projects with shared Kotlin code targeting JVM, JS, Native, and Android. Dependency declarations include Kotlin standard library, kotlinx libraries (coroutines, serialization, datetime), and substantial third-party Maven Central / Google packages. The combination — Gradle as the conventional build, the Kotlin DSL for builds, the substantial Maven repository ecosystem, the Multiplatform mechanism, the expect/actual for platform-specific code — is the substance of Kotlin’s package management.
A Kotlin project
The conventional layout:
my-project/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle/
│ └── wrapper/
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── src/
├── main/
│ ├── kotlin/
│ │ └── com/example/
│ │ └── Main.kt
│ └── resources/
└── test/
└── kotlin/
└── com/example/
└── MainTest.kt
build.gradle.kts
The Kotlin DSL build script:
plugins {
kotlin("jvm") version "2.1.0"
application
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("io.ktor:ktor-client-core:2.3.5")
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
application {
mainClass.set("com.example.MainKt")
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(17)
}
The principal sections:
plugins— Gradle plugins applied (Kotlin, Application, etc.).group/version— project identification.repositories— where to find dependencies (Maven Central, Google, custom).dependencies— runtime, test, compile-only dependencies.tasks— task configuration (test, jar, etc.).
Gradle commands
# Initial setup:
gradle wrapper # generate wrapper
# Build:
./gradlew build # build everything
./gradlew assemble # produce artifacts (no tests)
./gradlew compileKotlin # compile main
./gradlew compileTestKotlin # compile tests
# Test:
./gradlew test # run tests
./gradlew test --tests "com.example.MyTest"
./gradlew test --tests "*.MyTest"
# Run:
./gradlew run # run application
./gradlew run --args="arg1 arg2"
# Cleanup:
./gradlew clean
# Dependencies:
./gradlew dependencies
./gradlew :app:dependencies # subproject
# Tasks:
./gradlew tasks # list tasks
./gradlew --help
Dependency configurations
The principal configurations:
| Configuration | Use |
|---|---|
implementation | Compile and runtime; not exposed to consumers |
api | Compile, runtime; exposed to consumers (libraries only) |
compileOnly | Compile only; not in runtime classpath |
runtimeOnly | Runtime only; not in compile classpath |
testImplementation | Test compile and runtime |
testCompileOnly / testRuntimeOnly | Test variants |
The conventional contemporary discipline favours implementation over api — admit substantial encapsulation.
Adding dependencies
dependencies {
// From Maven Central:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// Kotlin standard library (typically auto-included):
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect"))
// Test:
testImplementation(kotlin("test"))
testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
testImplementation("io.mockk:mockk:1.13.8")
}
For version catalogs (since Gradle 7+):
// gradle/libs.versions.toml
[versions]
kotlin = "2.1.0"
coroutines = "1.8.0"
ktor = "2.3.5"
[libraries]
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
// build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
}
dependencies {
implementation(libs.kotlinx.coroutines)
implementation(libs.ktor.client)
}
Kotlin plugins
The principal Kotlin plugins:
plugins {
kotlin("jvm") version "2.1.0" // Kotlin/JVM
kotlin("multiplatform") version "2.1.0" // Kotlin Multiplatform
kotlin("js") version "2.1.0" // Kotlin/JS
kotlin("native") version "2.1.0" // Kotlin/Native (typically via multiplatform)
kotlin("plugin.serialization") version "2.1.0" // kotlinx.serialization
kotlin("plugin.spring") version "2.1.0" // Spring integration
kotlin("kapt") version "2.1.0" // annotation processor
}
Multi-module projects
For substantial projects, multiple modules:
// settings.gradle.kts
rootProject.name = "my-project"
include(":app", ":lib", ":core")
// app/build.gradle.kts
dependencies {
implementation(project(":lib"))
implementation(project(":core"))
}
// lib/build.gradle.kts
dependencies {
implementation(project(":core"))
}
The conventional layout:
my-project/
├── settings.gradle.kts
├── build.gradle.kts # root config
├── app/
│ ├── build.gradle.kts
│ └── src/main/kotlin/
├── lib/
│ ├── build.gradle.kts
│ └── src/main/kotlin/
└── core/
├── build.gradle.kts
└── src/main/kotlin/
Application plugin
For executable projects:
plugins {
application
}
application {
mainClass.set("com.example.MainKt")
applicationDefaultJvmArgs = listOf("-Xmx2g")
}
The mainClass references the synthetic MainKt class containing the top-level main function.
For multiple entry points:
val createServerScript by tasks.creating(CreateStartScripts::class) {
mainClass.set("com.example.ServerKt")
applicationName = "server"
classpath = tasks.jar.get().outputs.files + configurations.runtimeClasspath
outputDir = file("$buildDir/scripts")
}
Library publishing
For publishing to Maven Central or similar:
plugins {
`maven-publish`
signing
id("io.github.gradle-nexus.publish-plugin") version "1.3.0"
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
pom {
name.set("My Library")
description.set("A useful library")
url.set("https://github.com/user/my-library")
licenses {
license {
name.set("Apache 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0")
}
}
}
}
}
}
Kotlin Multiplatform
For substantial cross-platform projects:
plugins {
kotlin("multiplatform") version "2.1.0"
}
kotlin {
jvm()
js(IR) { browser(); nodejs() }
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting {
dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.5")
}
}
val jsMain by getting {
dependencies {
implementation("io.ktor:ktor-client-js:2.3.5")
}
}
val iosMain by creating {
dependsOn(commonMain)
// shared iOS code
}
}
}
The source-set layout:
src/
├── commonMain/kotlin/ # shared across platforms
├── commonTest/kotlin/
├── jvmMain/kotlin/ # JVM-specific
├── jsMain/kotlin/ # JS-specific
├── iosMain/kotlin/ # iOS shared
├── iosX64Main/kotlin/ # iOS Intel-specific
└── iosArm64Main/kotlin/ # iOS ARM-specific
expect and actual
For platform-specific implementations:
// commonMain:
expect class Platform {
val name: String
fun describe(): String
}
// jvmMain:
actual class Platform {
actual val name: String = "JVM"
actual fun describe(): String = "Running on JVM ${System.getProperty("java.version")}"
}
// jsMain:
actual class Platform {
actual val name: String = "JS"
actual fun describe(): String = "Running on JavaScript"
}
// iosMain:
actual class Platform {
actual val name: String = "iOS"
actual fun describe(): String = "Running on iOS"
}
The mechanism admits substantial cross-platform code with platform-specific implementations.
Common patterns
Spring Boot project
plugins {
id("org.springframework.boot") version "3.2.0"
id("io.spring.dependency-management") version "1.1.0"
kotlin("jvm") version "2.1.0"
kotlin("plugin.spring") version "2.1.0"
kotlin("plugin.jpa") version "2.1.0"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
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")
}
Ktor server
plugins {
kotlin("jvm") version "2.1.0"
kotlin("plugin.serialization") version "2.1.0"
application
}
dependencies {
implementation("io.ktor:ktor-server-core:2.3.5")
implementation("io.ktor:ktor-server-netty:2.3.5")
implementation("io.ktor:ktor-server-content-negotiation:2.3.5")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5")
testImplementation("io.ktor:ktor-server-test-host:2.3.5")
testImplementation(kotlin("test"))
}
application {
mainClass.set("com.example.MainKt")
}
Android project
plugins {
id("com.android.application")
kotlin("android")
id("com.google.dagger.hilt.android")
}
android {
namespace = "com.example.app"
compileSdk = 34
defaultConfig {
applicationId = "com.example.app"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
kotlinOptions {
jvmTarget = "17"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
}
Library with API surface
plugins {
`java-library`
kotlin("jvm") version "2.1.0"
`maven-publish`
}
dependencies {
api(libs.kotlinx.coroutines) // exposed to consumers
implementation(libs.kotlinx.serialization) // hidden from consumers
testImplementation(kotlin("test"))
}
kotlin {
explicitApi() // require explicit visibility
}
The explicitApi() admits enforcing public-API discipline.
Test setup with Kotest
plugins {
kotlin("jvm") version "2.1.0"
}
dependencies {
testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
testImplementation("io.kotest:kotest-assertions-core:5.8.0")
testImplementation("io.kotest:kotest-property:5.8.0")
}
tasks.test {
useJUnitPlatform()
}
// Test code:
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class CalculatorTest : StringSpec({
"addition works" {
2 + 2 shouldBe 4
}
"division by zero throws" {
shouldThrow<ArithmeticException> {
10 / 0
}
}
})
Build for production
tasks.shadowJar { // requires shadow plugin
archiveBaseName.set("my-app")
archiveClassifier.set("")
archiveVersion.set("")
}
application {
mainClass.set("com.example.MainKt")
}
./gradlew shadowJar # produces fat JAR
java -jar build/libs/my-app.jar
Cross-platform library
plugins {
kotlin("multiplatform") version "2.1.0"
kotlin("plugin.serialization") version "2.1.0"
}
kotlin {
jvm()
js(IR) { browser() }
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
}
}
}
Reproducible builds
tasks.withType<AbstractArchiveTask> {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}
Custom task
tasks.register("hello") {
doLast {
println("Hello from Kotlin!")
}
}
./gradlew hello
A note on the conventional discipline
The contemporary Kotlin packages-and-Gradle advice:
- Use the Kotlin DSL (
build.gradle.kts) for new projects. - Use Gradle wrapper — admit substantial reproducibility.
- Use version catalogs —
gradle/libs.versions.tomlfor centralised versions. - Use
implementationoverapi— admit substantial encapsulation. - Use multi-module structure for substantial projects.
- Use Kotlin Multiplatform for substantial cross-platform code.
- Use
expect/actualfor platform-specific implementations. - Use Maven Central as the conventional repository.
- Use
kotlin("test")for portable test setup. - Use
applicationplugin for executable projects. - Use
explicitApi()in libraries for substantial visibility discipline.
The combination — Gradle as the conventional build system, the Kotlin DSL for builds, the substantial Maven Central ecosystem, the multi-module support, the Multiplatform mechanism with expect/actual — is the substance of Kotlin’s package management. The discipline produces clear, well-organised projects with substantial cross-platform support and substantial dependency management.