Skip to content
Published on

Modern Kotlin 2026 — Kotlin 2.1 / K2 Compiler / Compose Multiplatform / KMP 2.1 / Ktor 3 / Spring Boot Kotlin Deep Dive

Authors

Prologue — In 2026, Kotlin is no longer "the Android language"

The day Kotlin became an official Android language at Google I/O 2017, it lived for years as "the Java alternative for Android." Server folks used Java 17, then 21. Multiplatform crawled from alpha to beta.

The picture in May 2026 is unrecognizable.

  • Kotlin 2.1 (Nov 2024) made the K2 compiler the default. IDE K2 mode is also default, and clean builds run on average 1.8x faster than K1.
  • Kotlin 2.2 (Mar 2025) introduced context parameters — the successor to the old context receivers. You can bind dependencies into a function signature without polluting the return type.
  • Compose Multiplatform 1.7 (Oct 2024) declared iOS stable. One codebase paints Android, iOS, desktop, and web (Wasm).
  • KMP (Kotlin Multiplatform) 2.1 is past "production-ready" — Mercari, McDonald's, and others have rewritten core SDKs in KMP.
  • Ktor 3 (Oct 2024) added a WebAssembly target and was rewritten on top of kotlinx-io; throughput rose ~90% on average.
  • Spring Boot 3.5 treats Kotlin as a first-class language. Coroutine support is mature and Kotlin DSL builds are standard.

This piece tries to take you from "I just started looking at Kotlin" to "I understand what it looks like in 2026 production" in a single read. Language, compiler, UI, server, library ecosystem, and real Korean/Japanese adoption — all of it.


1. Modern Kotlin in 2026 — where Kotlin 2.x sits

First, set coordinates. Here's the Kotlin full stack as of May 2026.

                +----------------------------------------+
                |             Kotlin 2.2.x               |
                |  (K2 compiler default, JVM/JS/Wasm)    |
                +----------------------------------------+
                              |
          +-------------------+--------------------+
          |                   |                    |
          v                   v                    v
   [JVM backend]      [Multiplatform]         [Native / Wasm]
   Spring Boot 3.5    KMP 2.1 + CMP 1.7       Kotlin/Native
   Ktor 3             iOS/Android/Desktop     Kotlin/Wasm
   gRPC-Kotlin        Web (Wasm)              (Ktor 3 server)
          |                   |                    |
          +-------------------+--------------------+
                              |
                              v
                  +-------------------------+
                  |    kotlinx libraries    |
                  | coroutines / Flow       |
                  | serialization / io      |
                  | datetime / atomicfu     |
                  +-------------------------+
                              |
                              v
              +---------------+----------------+
              |               |                |
              v               v                v
         [DI/FP/ORM]    [Static analysis]    [Build]
         Koin / Arrow    Detekt / Konsist    Gradle Kotlin DSL
         Exposed         Kotest / MockK      Maven Kotlin plugin

The summary points:

  • The language ships a major every ~2 years (1.x to 2.x), a minor every 6 months.
  • Multiplatform is no longer "experimental." iOS stable was the game changer.
  • The server ecosystem is alive with both Spring Boot and Ktor.
  • The library ecosystem (Koin/Arrow/Exposed/Kotest/MockK) is thick enough to ship a Kotlin-idiomatic full stack.

We'll walk each box in depth.


2. Kotlin 2.1 (Nov 2024) — K2 default + multi-dollar strings

If Kotlin 2.0 was the first release where K2 became stable, 2.1 is the one that made it default for everyone. JetBrains also flipped IntelliJ's K2 mode on by default. K1 fallback is supported only until late 2025.

The 2.1 headline features:

  1. K2 compiler default — anything at -language-version 2.0 and above is K2.
  2. Multi-dollar ($$, $$$) string interpolation — escape from $ escaping when dealing with templates or LaTeX.
  3. when expression guard conditionswhen (x) { is Foo if x.bar > 10 -> ... } lets you add an extra check inline.
  4. .kotlin cache directory — build outputs live under the project root, like Gradle's .gradle.
  5. Non-local break/continue — you can break an outer loop from inside an inline lambda (preview).

Multi-dollar strings are genuinely nice.

// Kotlin 2.1: inside $$"..."$$, a single $ is literal; $$ starts interpolation
val template = $$"""
    Hello, $$name!
    Your balance is $1000 (USD).
    JSON: {"value": "$$value"}
"""

Before, every literal $ needed escaping or you had to splice raw strings together. Now it's one line.

when guards get used constantly:

sealed interface Order
data class Pending(val amount: Int) : Order
data class Paid(val amount: Int, val txId: String) : Order

fun handle(order: Order) = when (order) {
    is Pending if order.amount > 10_000 -> "high-value pending"
    is Pending -> "pending"
    is Paid -> "paid: ${order.txId}"
}

Under K1 you had to nest an if inside the is Pending branch, which broke the exhaustive check. Now it stays on one line, exhaustive intact.


3. Kotlin 2.2 (Mar 2025) — context parameters

The headline of Kotlin 2.2 is context parameters — the official successor to "context receivers," which had been in beta since 1.7.

What problem does it solve

Sometimes you want to inject dependencies into a function implicitly. For example:

// Before: always pass logger explicitly
fun calculate(x: Int, y: Int, logger: Logger): Int {
    logger.info("calc $x + $y")
    return x + y
}

// Or wrap in a class
class Calculator(private val logger: Logger) {
    fun calculate(x: Int, y: Int): Int { ... }
}

Context parameters are the middle road: declare the dependency in the signature, but the caller doesn't have to pass it explicitly.

// Kotlin 2.2 context parameters
context(logger: Logger)
fun calculate(x: Int, y: Int): Int {
    logger.info("calc $x + $y")
    return x + y
}

// Caller: bring a Logger into scope and it's auto-injected
fun main() {
    with(MyLogger()) {
        val result = calculate(1, 2)  // no explicit logger
    }
}

How is it different from context receivers

The old 1.x context(Logger) form had no name. Two contexts of the same type clashed. 2.2's context(logger: Logger) is named. Arrow's effect library, Compose's CompositionLocal, and structured logging libraries are all being rewritten on top.

Real pattern: Arrow's raise

import arrow.core.raise.Raise

context(raise: Raise<String>)
fun parsePositive(s: String): Int {
    val n = s.toIntOrNull() ?: raise.raise("not a number: $s")
    if (n <= 0) raise.raise("must be positive: $n")
    return n
}

// Caller
fun main() {
    val result = either {
        val n = parsePositive("42")
        n * 2
    }
    // result: Either.Right(84)
}

You can compose an error channel without exposing Either<E, A> everywhere. This is the Kotlin answer to Scala's ZIO or Haskell's monad transformer style of effect systems.


4. The K2 compiler — what gets faster, what changes

K2 is the result of a complete rewrite of the compiler frontend. K1 was designed in the early 2010s, and its smart-cast / generic inference code was scattered across many passes. K2 unified everything around a single intermediate representation called FIR (Frontend IR).

Practical impact:

MetricK1K2Note
Clean build1.0x1.6 - 1.9x fasterLarger projects see bigger gains
Incremental build1.0x2.0 - 2.4x fasterBetter cache / invalidation
IDE highlight1.0x1.5x fasterSmart-cast / inference caching
Peak memory1.0x~0.7xFIR dedup

Numbers blend JetBrains' published benchmarks with migration reports from Mercari, Square, and friends.

Smart cast gets smarter

K1 struggled with consistent narrowing across complex branches.

fun process(value: Any?) {
    if (value is String || value is Int) {
        // K1: value is still Any?
        // K2: value is String | Int (intersection)
        println(value.hashCode())  // works on K2
    }
}

K2 also recognizes delayed val initialization.

class Holder {
    val data: List<Int>
    init {
        val tmp = mutableListOf<Int>()
        tmp.add(1); tmp.add(2)
        data = tmp.toList()  // K2: OK
    }
}

Compiler plugin API stabilization

The Compose compiler plugin, kotlinx-serialization, and Arrow's raise plugin have all been rewritten against K2 FIR. Build times dropped, IDE indexing stabilized.

Migration checklist

  • Bump kotlin.languageVersion = "2.1".
  • kotlin.compiler.execution.strategy = in-process (Gradle daemon).
  • Compose projects use compose-compiler-gradle-plugin (shipped directly by JetBrains since 2024).
  • Drop legacy flags like -Xuse-fir-lt.

5. Compose Multiplatform — iOS stable!

Compose Multiplatform 1.7, released October 2024, was a real milestone: iOS went stable. Up until then Compose-Android was production-grade but iOS was beta. From 1.7 onward, iOS is OK for production.

1.7 headlines (Oct 2024) plus the 1.7+ trickle

  • iOS stable — UIViewController integration, text input, accessibility, dark mode all stable.
  • Resource API 1.0 — type-safe Res.drawable.icon / Res.string.welcome proxies.
  • Adaptive layoutsWindowSizeClass API. Phone/tablet/foldable branches.
  • Skia 0.8 to 0.10 — better text rendering quality and lower GPU memory.
  • Hot reload (desktop) — leverages Java 21's enhanced class redefinition. The design loop is genuinely fast.

One screen, four platforms

// commonMain/HelloApp.kt
@Composable
fun HelloApp(name: String) {
    MaterialTheme {
        Column(modifier = Modifier.padding(16.dp)) {
            Text("Hello, $name!", style = MaterialTheme.typography.headlineSmall)
            Spacer(Modifier.height(8.dp))
            Button(onClick = { /* ... */ }) {
                Text("Tap me")
            }
        }
    }
}

The exact same code runs on Android, iOS, macOS/Windows/Linux desktop, and (experimentally) Web Wasm. Only platform-specific bits get extracted via expect/actual.

// commonMain
expect fun platformName(): String

// androidMain
actual fun platformName(): String = "Android ${Build.VERSION.RELEASE}"

// iosMain
actual fun platformName(): String =
    UIDevice.currentDevice.systemName + " " + UIDevice.currentDevice.systemVersion

Compose vs SwiftUI in 2026

ItemCompose Multiplatform 1.7+SwiftUI
PlatformsAndroid / iOS / Desktop / WebApple only
LanguageKotlinSwift
Code share100% (down to UI)0% (other platforms)
iOS performance95% (some scroll / animation gaps remain)100% native
Native widget accessUIViewControllerRepresentable-style interopNatural
Build timeKMP build overheadStandard Xcode

Recommendation:

  • iOS only → SwiftUI.
  • Android + iOS, design consistency matters → CMP.
  • "Use every new Apple API the day it ships" → SwiftUI.

6. KMP 2.1 — production-ready

If Compose Multiplatform is about UI, KMP (Kotlin Multiplatform) is about logic sharing. Business logic, networking, disk cache, domain model — write it once in Kotlin, reuse it on Android, iOS, server, and web.

KMP module layout

shared/
  build.gradle.kts
  src/
    commonMain/kotlin/    <- code shared by every platform
    androidMain/kotlin/   <- Android-only
    iosMain/kotlin/       <- iOS-only (Kotlin/Native)
    jvmMain/kotlin/       <- JVM server
    wasmJsMain/kotlin/    <- Web Wasm
    commonTest/kotlin/    <- shared tests

Core libraries

  • kotlinx.coroutines — same API across every platform.
  • kotlinx.serialization — JSON/Protobuf/CBOR. Multiplatform standard.
  • Ktor Client — multiplatform HTTP/WebSocket.
  • SQLDelight 2 — type-safe SQL driver across iOS/Android/JVM/Wasm.
  • Multiplatform Settings — abstracts UserDefaults / SharedPreferences / files.
  • Decompose / Voyager — multiplatform navigation.
  • Koin — DI, multiplatform.

iOS integration — taking a KMP artifact into Xcode

// shared/build.gradle.kts
kotlin {
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    cocoapods {
        version = "1.0"
        summary = "Shared module for MyApp"
        homepage = "https://example.com"
        ios.deploymentTarget = "15.0"
        framework {
            baseName = "Shared"
            isStatic = false
        }
    }
}
./gradlew :shared:podPublishXcFramework
# Add the artifact to the Xcode project's Podfile

Swift just imports it.

import Shared

let repo = UserRepository()
Task {
    let users = try await repo.fetchUsers()
    print(users)
}

A Kotlin suspend fun is auto-mapped to Swift async throws (thanks to 2.1 plus Kotlin/Native's swift-export improvements).

Mercari, McDonald's, and other case studies

  • Mercari — migrated search and payment core logic to KMP. Bug fixes on Android and iOS happen in one place.
  • McDonald's — rebuilt the Global Mobile App on KMP.
  • See section 13 for a deeper look at Korean and Japanese companies.

7. Ktor 3 (Oct 2024) — WebAssembly target

Ktor 3 landed in October 2024 and is not "a minor bump on v2" — it's a near-rewrite of the framework.

Headlines

  1. Rewritten on kotlinx-io — dropped okio / java.nio dependencies. Throughput up ~90% on average.
  2. WebAssembly target — you can build a Ktor server with Kotlin/Wasm. Edge-compute use cases.
  3. CIO engine HTTP/2 stable — no external dependency for HTTP/2.
  4. Server-Sent Events plugininstall(SSE).
  5. WebSocket extensions — compression and ping/pong reworked.

Minimal Ktor 3 server

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.1.0"
    application
}
dependencies {
    implementation("io.ktor:ktor-server-core:3.0.0")
    implementation("io.ktor:ktor-server-cio:3.0.0")
    implementation("io.ktor:ktor-server-content-negotiation:3.0.0")
    implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.0")
}
// src/main/kotlin/Application.kt
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respond
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import kotlinx.serialization.Serializable

@Serializable
data class User(val id: Long, val name: String)

fun main() {
    embeddedServer(CIO, port = 8080) {
        install(ContentNegotiation) { json() }
        routing {
            get("/users") {
                call.respond(listOf(User(1, "Alice"), User(2, "Bob")))
            }
        }
    }.start(wait = true)
}

./gradlew run and you get JSON at 8080. Coroutine-based, so one thread can handle thousands of connections instead of one-thread-per-request.

Spring Boot vs Ktor — picking one

ItemKtor 3Spring Boot 3.5 + Kotlin
PhilosophyMinimal, opt-in plugins"Convention over config"
ConcurrencyCoroutine nativeWebFlux (reactive) or virtual threads
EcosystemLeanMassive, everything exists
Cold startFastFast with GraalVM native image
Learning curveShortLong but very well documented
Best fitMicroservices, edge, MVPLarge monolith, enterprise

8. Spring Boot + Kotlin — JVM server

When you go to write a JVM service in Kotlin, 70% of companies are still on Spring Boot. And the Spring team did not ignore that traffic. Spring Framework 6 and the Boot 3.x line have promoted Kotlin to first-class.

Modern Spring Boot + Kotlin stack (2026)

  • Spring Boot 3.5 (Java 21 baseline, Kotlin 2.1+).
  • Spring WebFlux + coroutines — controllers accept suspend functions directly.
  • R2DBC + Kotlin coroutines — async DB access.
  • Spring Security 6 — Kotlin DSL.
  • Spring AOT + GraalVM native image — cold start under 100 ms.

Coroutine controller

@RestController
class UserController(private val repo: UserRepository) {
    @GetMapping("/users/{id}")
    suspend fun getUser(@PathVariable id: Long): UserDto =
        repo.findById(id)?.toDto() ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)

    @GetMapping("/users", produces = [MediaType.APPLICATION_NDJSON_VALUE])
    fun streamUsers(): Flow<UserDto> =
        repo.findAll().map { it.toDto() }
}

suspend fun directly. No need to convert WebFlux Mono / Flux. A Flow<UserDto> is auto-mapped to an NDJSON streaming response.

Coroutine repository (R2DBC)

interface UserRepository : CoroutineCrudRepository<UserEntity, Long> {
    suspend fun findByEmail(email: String): UserEntity?

    @Query("SELECT * FROM users WHERE created_at > :since")
    fun findRecentUsers(since: Instant): Flow<UserEntity>
}

Kotlin DSL configuration

@Configuration
class AppConfig {
    @Bean
    fun routes(handler: UserHandler) = coRouter {
        accept(MediaType.APPLICATION_JSON).nest {
            GET("/users", handler::list)
            POST("/users", handler::create)
        }
    }

    @Bean
    fun securityFilter(http: ServerHttpSecurity): SecurityWebFilterChain =
        http {
            authorizeExchange {
                authorize("/public/**", permitAll)
                authorize(anyExchange, authenticated)
            }
            oauth2ResourceServer { jwt {} }
        }
}

http { ... } is the Kotlin DSL. The XML / Java config boilerplate is gone.


9. kotlinx.coroutines + Flow — the async model

Kotlin's async model has two axes. Coroutines (single async operations that finish) and Flow (streams of values over time).

Coroutines basics

import kotlinx.coroutines.*

suspend fun fetchUser(id: Long): User { /* HTTP */ }
suspend fun fetchOrders(userId: Long): List<Order> { /* HTTP */ }

suspend fun loadProfile(id: Long): Profile = coroutineScope {
    val userDeferred = async { fetchUser(id) }
    val ordersDeferred = async { fetchOrders(id) }
    Profile(userDeferred.await(), ordersDeferred.await())
}

Inside coroutineScope, the two tasks run in parallel, and if either fails the other is canceled — structured concurrency.

Flow — cold stream

fun userUpdates(userId: Long): Flow<UserEvent> = flow {
    while (currentCoroutineContext().isActive) {
        val event = pollEvent(userId)
        if (event != null) emit(event)
        delay(1000)
    }
}

// Consume
suspend fun watchUser() {
    userUpdates(42L)
        .filter { it.type == EventType.UPDATED }
        .map { it.toDto() }
        .take(10)
        .collect { println(it) }
}

Flow is cold — every .collect call replays from the start. For hot streams use SharedFlow / StateFlow.

StateFlow — UI state holder

class UserViewModel(private val repo: UserRepository) {
    private val _state = MutableStateFlow<UserState>(UserState.Loading)
    val state: StateFlow<UserState> = _state.asStateFlow()

    fun load(id: Long) {
        viewModelScope.launch {
            _state.value = UserState.Loading
            _state.value = runCatching { repo.findById(id) }
                .fold(
                    onSuccess = { UserState.Loaded(it) },
                    onFailure = { UserState.Error(it.message ?: "") }
                )
        }
    }
}

In Compose you do val state by viewModel.state.collectAsState() and render.

Coroutine debugger

In IntelliJ: Debug menu → Coroutines Dump. You see exactly which coroutine is suspended where, as a tree. Compared to a Java thread dump, it's not even close.


10. Detekt / Konsist — static analysis + architecture testing

Two tools that automate Kotlin code quality.

Detekt — code smell static analysis

// build.gradle.kts
plugins {
    id("io.gitlab.arturbosch.detekt") version "1.23.7"
}

detekt {
    toolVersion = "1.23.7"
    config.setFrom("$rootDir/detekt.yml")
    buildUponDefaultConfig = true
}

Excerpt from detekt.yml:

complexity:
  CyclomaticComplexMethod:
    threshold: 15
  LongMethod:
    threshold: 60
naming:
  FunctionNaming:
    functionPattern: '[a-z][a-zA-Z0-9]*'
style:
  MagicNumber:
    ignoreNumbers: ['-1', '0', '1', '2']

CI integration:

./gradlew detekt
# Output at build/reports/detekt/detekt.html

K2 compatibility has been stable since 1.23, and from 1.24 (mid-2025) K2-only rules were added (smart-cast hints, sealed exhaustive checks, and so on).

Konsist — architecture testing

The Kotlin counterpart to ArchUnit. Verifies layering, naming, and annotation usage like a unit test.

// src/test/kotlin/ArchitectureTest.kt
import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.verify.assertTrue

class ArchitectureTest {
    @Test
    fun `repositories should not depend on controllers`() {
        Konsist.scopeFromProject()
            .classes()
            .withNameEndingWith("Repository")
            .assertTrue { repo ->
                repo.containingFile.imports
                    .none { it.name.contains("controller") }
            }
    }

    @Test
    fun `all use cases end with UseCase`() {
        Konsist.scopeFromPackage("..usecase..")
            .classes()
            .assertTrue { it.name.endsWith("UseCase") }
    }
}

Runs as part of ./gradlew test. A new developer who calls a repository from a controller gets a red CI.

Detekt is "quality inside a file." Konsist is "structure across the project." They complement each other.


11. Koin / Arrow / Exposed / Kotest / MockK — the library ecosystem

The Kotlin-native counterparts to Java's Spring / Hibernate / Mockito.

Koin — DSL-based DI

val appModule = module {
    single<UserRepository> { UserRepositoryImpl(get()) }
    single<HttpClient> {
        HttpClient(CIO) {
            install(ContentNegotiation) { json() }
        }
    }
    viewModel { UserViewModel(get()) }
}

fun main() {
    startKoin {
        modules(appModule)
    }
    // ...
}

No reflection. No compile-time codegen (unlike Dagger). KMP-compatible. Used directly inside Compose Multiplatform.

Arrow — Kotlin's de facto FP library

import arrow.core.Either
import arrow.core.raise.either

sealed interface UserError {
    object NotFound : UserError
    data class Invalid(val reason: String) : UserError
}

fun parseAge(s: String): Either<UserError, Int> = either {
    val n = s.toIntOrNull() ?: raise(UserError.Invalid("not a number: $s"))
    if (n < 0 || n > 150) raise(UserError.Invalid("age out of range: $n"))
    n
}

fun loadUser(id: Long): Either<UserError, User> = either {
    val user = repo.find(id) ?: raise(UserError.NotFound)
    user
}

fun process(id: Long, ageStr: String): Either<UserError, Result> = either {
    val user = loadUser(id).bind()
    val age = parseAge(ageStr).bind()
    Result(user, age)
}

Compose errors with Either<L, R> plus the raise DSL — no throws. Combine that with 2.2 context parameters and you get a complete effect system.

Exposed — type-safe SQL DSL

object Users : LongIdTable("users") {
    val name = varchar("name", 100)
    val email = varchar("email", 200).uniqueIndex()
    val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
}

transaction {
    val newId = Users.insertAndGetId {
        it[name] = "Alice"
        it[email] = "alice@example.com"
    }

    val users = Users
        .selectAll()
        .where { Users.name like "A%" }
        .orderBy(Users.createdAt to SortOrder.DESC)
        .limit(10)
        .map { it[Users.name] to it[Users.email] }
}

Lighter than JPA, no Hibernate lazy-loading traps. The R2DBC adapter (stable since 1.0) lets it run in coroutine environments.

Kotest — BDD-style testing

class UserSpec : StringSpec({
    "user with valid email should be created" {
        val u = User("alice@example.com")
        u.isValid shouldBe true
    }

    "negative age should throw" {
        shouldThrow<IllegalArgumentException> {
            User("a@b", age = -1)
        }
    }
})

class PropertySpec : StringSpec({
    "reverse twice is identity" {
        checkAll<List<Int>> { list ->
            list.reversed().reversed() shouldBe list
        }
    }
})

JUnit-compatible, expression-based assertions, property-based testing (QuickCheck style) built in. Multiplatform.

MockK — pure Kotlin mocking

class UserServiceTest : StringSpec({
    val repo = mockk<UserRepository>()
    val service = UserService(repo)

    "should return user from repo" {
        coEvery { repo.findById(1L) } returns User(1, "Alice")

        val u = service.getUser(1L)
        u.name shouldBe "Alice"

        coVerify(exactly = 1) { repo.findById(1L) }
    }
})

Mocks the things Mockito can't: object, companion object, top-level functions, suspend functions.


12. Gradle Kotlin DSL — the build standard

As of 2026, 99% of new Kotlin projects use the Gradle Kotlin DSL (build.gradle.kts). The Groovy DSL is legacy.

Minimal build.gradle.kts

plugins {
    kotlin("jvm") version "2.1.0"
    kotlin("plugin.serialization") 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")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
}

application {
    mainClass.set("com.example.AppKt")
}

kotlin {
    jvmToolchain(21)
}

tasks.test {
    useJUnitPlatform()
}

Multi-module + version catalog

gradle/libs.versions.toml:

[versions]
kotlin = "2.1.0"
coroutines = "1.9.0"
ktor = "3.0.0"

[libraries]
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }

Per-module build.gradle.kts:

plugins {
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.ktor)
}
dependencies {
    implementation(libs.ktor.server.core)
    implementation(libs.ktor.server.cio)
    implementation(libs.kotlinx.coroutines)
}

Single source of version truth. The payoff is huge once you cross 30 modules.

Configuration cache + build cache

# gradle.properties
org.gradle.configuration-cache=true
org.gradle.caching=true
org.gradle.parallel=true
kotlin.incremental=true
kotlin.code.style=official

The configuration cache went stable in 8.x. Builds run 2 to 5 times faster (especially incremental).

Is Maven dead?

No. Enterprise Spring Boot shops with large Maven estates still use the Maven Kotlin plugin.

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <version>2.1.0</version>
    <configuration>
        <jvmTarget>21</jvmTarget>
        <compilerPlugins>
            <plugin>spring</plugin>
            <plugin>jpa</plugin>
        </compilerPlugins>
    </configuration>
</plugin>

Functionally equivalent. Slower than Gradle but rock-solid.


13. Korea / Japan — Kakao, Toss, Mercari, ZOZO, CyberAgent

Korea

Kakao (Android-wide Kotlin)

  • 95%+ of the KakaoTalk Android codebase is Kotlin. New Java code is banned.
  • 100+ modules in a multi-module setup, Gradle Kotlin DSL plus version catalog.
  • Internally published Detekt config + a Kakao house style (KakaoCodeStyle).
  • Compose migration in progress (as of 2026, all new screens are 100% Compose).

Toss (server-side Kotlin pioneer)

  • 100% of new backend microservices are Kotlin + Spring Boot. 200+ services on Kotlin.
  • Coroutines + WebFlux + R2DBC + Arrow is the standard combo.
  • Many in-house Kotlin DSL libraries (such as tossopen-kotlin-commons).
  • The Toss engineering blog has multiple series on "Kotlin migration for Spring" and "Why Kotlin instead of Java."

LINE (NAVER) — KMP cases

  • Pieces of the LINE messenger's client SDK are being shared via KMP. Partial production adoption as of 2026.

Japan

Mercari — KMP pioneer

  • Rebuilt search and payment core SDKs on KMP. Fixes ship to Android and iOS at once.
  • 2025 announcement: "We migrated 60% of our shared business logic to KMP."
  • Currently evaluating Compose Multiplatform 1.7+.

ZOZOTOWN (ZOZO)

  • Android is full Kotlin. Parts of the server are Kotlin (Spring Boot + coroutines).
  • In-house "Kotlin Guild" — biweekly study sessions, internal library sharing.

CyberAgent (AbemaTV, FRIDAY, ...)

  • New Android projects are 100% Kotlin.
  • AbemaTV Android — Compose + coroutines + Koin + Coil.
  • A few microservices on Ktor 3 (streaming metadata service).

DeNA / GREE / Rakuten

  • Same pattern: Android fully Kotlin, server selectively Kotlin.

Common patterns

Across Korean and Japanese large companies:

  1. Android is already 100% Kotlin.
  2. Server adoption begins with new microservices.
  3. KMP is applied selectively, around core SDKs (full-app KMP is still rare).
  4. House style guides are Detekt-based + custom rules.
  5. Hiring: knowing Kotlin = Android + server, double the surface area.

14. Who should choose Kotlin — Android / KMP / JVM server / FP

A decision matrix to wrap up. Scenario-by-scenario answers to "is Kotlin the right call?"

"I'm writing an Android app"

Kotlin is the default. There's no other realistic choice.

  • Android Studio is Kotlin-first.
  • Jetpack Compose's primary language.
  • Every new Google library / document is Kotlin first.

"iOS and Android both, one team"

Consider KMP + Compose Multiplatform.

Conditions:

  • You don't need every new iOS API the day it lands (one to two months of lag on SwiftUI / AppKit features is OK).
  • The business logic is identical on both platforms.
  • iOS designers don't insist "this must be exactly iOS-native."

If all three hold, KMP. If any of them fails, ship two native apps.

"JVM server in a Spring Boot shop"

Kotlin beats Java on nearly every axis.

  • Null safety catches NPEs at compile time.
  • Coroutines are far more readable than CompletableFuture.
  • Data classes remove the need for Lombok.
  • Spring 6 / Boot 3.5 treat Kotlin as first-class.

Exception: if 80% of the team only knows Java, adopt incrementally (start with test code, then new microservices).

"Lightweight microservices, GraalVM native image"

Ktor 3 + Kotlin/Native, or Ktor 3 + GraalVM.

  • Cold start under 100 ms.
  • Memory under 50 MB.
  • Great fit for lambdas / edge functions.

"Serious functional programming"

Kotlin + Arrow. But the learning curve is steep.

  • Either / IO / context parameters / raise DSL combined.
  • Not as deep as Scala or Haskell, but the industry-friendly FP sweet spot on the JVM.

Scala is still ahead. Kotlin is supplementary.

  • The Spark Kotlin API exists but is not as mature as Scala's.
  • That said, newer tools like KEDB (Kotlin Embedded DB) are Kotlin-first.

"Not Android, not JVM"

→ Kotlin doesn't really need to be on your radar. JS / Wasm targets exist but TypeScript / Rust are the defaults there.


References

Kotlin language and compiler

Compose Multiplatform / KMP

Ktor / Spring

kotlinx libraries

Static analysis, testing, DI, FP, ORM

Gradle / build

Company case studies

One closing line. Kotlin 2.x is no longer "a slightly nicer Java for Android." The compiler (K2), UI (Compose Multiplatform), multiplatform (KMP), server (Spring / Ktor), and library ecosystem (Koin / Arrow / Exposed / Kotest) are now aligned at stable. It's the default language of the 2026 JVM ecosystem. Hopefully this piece helped you place the coordinates.