Polyglot
Languages Kotlin modules
Kotlin § modules

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:

ConfigurationUse
implementationCompile and runtime; not exposed to consumers
apiCompile, runtime; exposed to consumers (libraries only)
compileOnlyCompile only; not in runtime classpath
runtimeOnlyRuntime only; not in compile classpath
testImplementationTest compile and runtime
testCompileOnly / testRuntimeOnlyTest 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 catalogsgradle/libs.versions.toml for centralised versions.
  • Use implementation over api — admit substantial encapsulation.
  • Use multi-module structure for substantial projects.
  • Use Kotlin Multiplatform for substantial cross-platform code.
  • Use expect/actual for platform-specific implementations.
  • Use Maven Central as the conventional repository.
  • Use kotlin("test") for portable test setup.
  • Use application plugin 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.