Guide

Kotlin fundamentals explained

A field technician opens the Harbor Fleet mobile app offline, logs a delivery exception, and syncs when LTE returns. The client and the sync API share typed models, compile-time null checks, and non-blocking I/O — often written in Kotlin, a statically typed language that runs on the JVM, compiles to JavaScript for browsers, and targets native binaries through Kotlin Multiplatform (KMP). Google made Kotlin the preferred language for Android in 2019; JetBrains designed it to interoperate cleanly with existing Java libraries while eliminating boilerplate and encoding null safety in the type system. On the server, frameworks like Ktor and Spring Boot with Kotlin DSLs deliver concise HTTP services without sacrificing the mature JVM ecosystem. This guide covers Kotlin targets and tooling, null-safe types, data and sealed classes, coroutines, extension functions, Gradle builds, backend patterns, a Harbor Fleet mobile sync API worked example, a language decision table, common pitfalls, and a production checklist — alongside our Java, C#, and Go language guides.

What Kotlin is: JVM, JS, Native, and tooling

Kotlin source (.kt files) compiles to bytecode for the JVM by default, so it calls any Java library and deploys in the same JAR/Docker images as Java services. The Kotlin compiler (kotlinc) also emits JavaScript (Kotlin/JS) for front-end modules and native binaries (Kotlin/Native) for iOS and embedded targets when using KMP shared modules.

IntelliJ IDEA (JetBrains) provides first-class Kotlin support; Android Studio bundles the same foundation. The standard library adds pragmatic utilities: let, apply, run, and scope functions that read like fluent pipelines instead of nested null checks. Kotlin releases align with language versions (2.0+ brings the K2 compiler with faster analysis); pin versions in Gradle alongside the JVM target (11, 17, or 21).

Interoperability with Java

  • Call Java from Kotlin — platform types (T!) appear when Java APIs return nullable references; annotate Java with @Nullable/@Nonnull or wrap calls in Kotlin null checks.
  • Call Kotlin from Java — top-level functions become FileNameKt static methods; use @JvmStatic on companion object members for cleaner Java call sites.
  • Gradual migration — add Kotlin files to existing Maven/Gradle Java projects one module at a time; no big-bang rewrite required.

Null safety and the type system

Kotlin’s headline feature is null safety at compile time. Reference types are non-null by default: String cannot hold null; String? can. The compiler rejects code that dereferences a nullable without a safe call (?.), Elvis fallback (?:), or explicit check.

fun greet(name: String?): String =
    return name?.trim()?.takeIf { it.isNotEmpty() }?.let { "Hello, $it" }
        ?: "Hello, guest"

Smart casts narrow types after is checks or null guards without manual casting. Value types (Int, Double, Boolean) are non-nullable primitives on the JVM; boxing happens only when used as generics. Prefer immutable val over var for locals and properties unless mutation is intentional.

Classes, objects, and interfaces

  • Primary constructors — declare properties in the class header; init blocks run after field initialization.
  • Data classesdata class Delivery(id: UUID, status: Status) auto-generates equals, hashCode, copy, and toString.
  • Sealed classes and interfaces — exhaustive when branches over closed hierarchies (network results, UI states) without default fall-through bugs.
  • Objects and companions — singletons via object; factory methods in companion object replace static-only Java patterns.

Coroutines and structured concurrency

Blocking threads on mobile radios and webhook handlers wastes memory. Kotlin coroutines are lightweight continuations scheduled on thread pools; suspend functions pause without blocking the underlying thread. Structured concurrency ties child jobs to parent scopes so cancellation propagates and leaks are visible at compile time.

suspend fun fetchPendingDeliveries(userId: String): List<Delivery> =
    withContext(Dispatchers.IO) {
        api.getDeliveries(userId)
    }

Use CoroutineScope with a supervisor job in Android ViewModel or Ktor request handlers. async/await parallelizes independent I/O; Flow models cold streams of database changes or WebSocket events. On the JVM, coroutines complement (and often replace) callback-heavy RxJava for new code. For CPU-bound work, keep Dispatchers.Default; never block inside Dispatchers.Main on Android.

Extension functions, DSLs, and collections

Extension functions add methods to existing types without inheritance: fun String.isValidTrackingId() reads like a native API. Jetpack Compose, Ktor routing, and Gradle Kotlin DSLs are built from extension receivers that make configuration blocks type-safe and autocomplete-friendly.

Kotlin’s standard library mirrors Java collections with immutable List, Map, and Set interfaces plus rich functional operators: map, filter, groupBy, fold. Sequences (asSequence()) lazily chain transforms on large collections without intermediate lists. For Java interop, convert with toList() when passing to APIs expecting java.util.List.

Build tooling: Gradle Kotlin DSL

Most Kotlin projects use Gradle with build.gradle.kts. Plugins apply the Kotlin JVM, Android, or Multiplatform targets; dependency versions centralize in libs.versions.toml (version catalogs).

  • Source setscommonMain, jvmMain, androidMain in KMP share business logic while keeping platform UI separate.
  • Compiler options — enable -Xjsr305=strict for stricter Java nullability; use freeCompilerArgs for opt-in APIs.
  • Testing — JUnit 5 on JVM; kotlinx-coroutines-test provides runTest and virtual time for coroutine unit tests.

Package fat JARs with the application plugin or Ktor’s installDist task; containerize on Eclipse Temurin 21 the same way as Java services.

Backend services with Ktor

Ktor is JetBrains’ asynchronous Kotlin web framework built on coroutines and Netty. Routes are extension functions on Routing; content negotiation serializes JSON with kotlinx.serialization (explicit @Serializable models) or Jackson for Java ecosystem parity.

routing {
    get("/deliveries/{id}") {
        val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest)
        val delivery = deliveryService.findById(id)
        call.respond(delivery ?: HttpStatusCode.NotFound)
    }
}

Ktor suits greenfield microservices and BFF (backend-for-frontend) layers. Existing Spring shops often add Kotlin gradually: Spring Boot 3 supports coroutines in WebFlux controllers and Kotlin DSL for bean configuration. Choose Ktor when you want a lightweight, Kotlin-native stack; choose Spring when you need the full enterprise integration catalog (Security, Data, Cloud).

Worked example: Harbor Fleet mobile sync API

Harbor Fleet technicians capture delivery photos and status codes offline. The sync API must accept batched events, deduplicate on client-generated IDs, and return server-side conflicts for manual resolution.

  1. Shared models — KMP commonMain defines @Serializable data class SyncEvent(val clientId: String, val deliveryId: String, val status: DeliveryStatus, val capturedAt: Instant) used by Android and the JVM server.
  2. Persistence — Exposed ORM or jOOQ on PostgreSQL with INSERT ... ON CONFLICT (client_id) DO NOTHING for idempotent sync.
  3. Ktor endpoint — POST /v1/sync accepts a JSON array; validate JWT from the fleet auth service; process in a CoroutineScope per request with timeout.
  4. Conflict detection — if server updated_at is newer than client capturedAt, return 409 with the canonical row for merge UI.
  5. Observability — Micrometer metrics for batch size and conflict rate; structured logs with delivery ID and technician ID (no PII in message bodies).
  6. Deploy — fat JAR in Docker behind nginx; health route /healthz; horizontal scale with stateless handlers.

Result: one language for Android offline queue logic and server ingestion, fewer translation bugs between DTOs, and compile-time guarantees that null status codes never reach production databases.

Language decision table

NeedPrefer KotlinConsider instead
New Android app (UI or KMP shared core)Yes — official Google recommendationJava only for legacy maintenance
JVM microservice with concise syntaxYes — Ktor or Spring Boot + KotlinJava for teams without Kotlin experience
Shared mobile + server models (KMP)Yes — common business logic moduleSeparate Swift/Kotlin with OpenAPI codegen
Smallest static binary CLINo — JVM or Native still heavier than GoGo or Rust
Browser-only front endRarely Kotlin/JS todayTypeScript / React
Hard real-time embeddedNoC, C++, Rust
Data science notebooksNoPython
Large legacy Spring monolith in Java onlyGradual Kotlin modulesStay Java until migration ROI is clear

Common pitfalls

  • Platform types from Java — treat JDK and Android SDK returns as nullable until annotated; enable strict JSR-305 mode in Gradle.
  • Blocking inside coroutinesThread.sleep or JDBC without R2DBC blocks the dispatcher; use withContext(Dispatchers.IO) or wrap blocking calls.
  • GlobalScope — fire-and-forget coroutines outlive their UI or request; always use structured scopes tied to lifecycle.
  • Data class misuse for entities — JPA entities need open classes and no-arg constructors; do not mark them data if the ORM requires mutable fields.
  • Overusing !! operatorvalue!! throws at runtime like Java NPE; prefer safe calls or early returns.
  • Serializable schema driftkotlinx.serialization requires explicit defaults for backward-compatible JSON; version your API contracts.
  • KMP premature adoption — shared modules add build complexity; prove Android+iOS need before splitting commonMain.
  • Ignoring Android ProGuard/R8 — keep serialization models and reflection entry points in keep rules for release builds.

Production checklist

  • Pin Kotlin and JVM versions in CI, Docker, and Gradle version catalogs.
  • Enable -Werror or detekt/ktlint in CI for consistent style.
  • Unit tests with JUnit 5; coroutine tests via runTest; Android instrumented tests for offline sync flows.
  • Structured logging (Logback JSON); correlation IDs from Ktor call IDs or Android interceptors.
  • Metrics via Micrometer; traces via OpenTelemetry Java agent on JVM services.
  • Dependency scanning (OWASP, Snyk); Renovate for kotlinx and Ktor security patches.
  • Graceful shutdown in Ktor ApplicationStopped hooks draining coroutine scopes.
  • Connection pools sized to Postgres limits; statement timeouts on JDBC paths.
  • Secrets from environment or vault — never committed in local.properties or application.conf.
  • Load-test sync batches with duplicate client IDs before peak delivery windows.

Key takeaways

  • Kotlin targets JVM, JS, and Native with Java interop and compile-time null safety.
  • Data classes, sealed types, and coroutines replace boilerplate Java patterns for mobile and server code.
  • Ktor and Spring Boot offer async HTTP on the JVM; KMP shares models across Android and backend when justified.
  • Choose Kotlin for new Android work, concise JVM services, and gradual Java modernization — not for tiny native CLIs or Python-centric data science.
  • Treat Java platform types and coroutine blocking as the main foot-guns; structure concurrency and test offline sync paths early.

Related reading