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/@Nonnullor wrap calls in Kotlin null checks. - Call Kotlin from Java — top-level functions become
FileNameKtstatic methods; use@JvmStaticoncompanion objectmembers 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 classes —
data class Delivery(id: UUID, status: Status)auto-generatesequals,hashCode,copy, andtoString. - Sealed classes and interfaces — exhaustive
whenbranches over closed hierarchies (network results, UI states) without default fall-through bugs. - Objects and companions — singletons via
object; factory methods incompanion objectreplace 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 sets —
commonMain,jvmMain,androidMainin KMP share business logic while keeping platform UI separate. - Compiler options — enable
-Xjsr305=strictfor stricter Java nullability; usefreeCompilerArgsfor opt-in APIs. - Testing — JUnit 5 on JVM;
kotlinx-coroutines-testprovidesrunTestand 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.
- Shared models — KMP
commonMaindefines@Serializable data class SyncEvent(val clientId: String, val deliveryId: String, val status: DeliveryStatus, val capturedAt: Instant)used by Android and the JVM server. - Persistence — Exposed ORM or jOOQ on
PostgreSQL
with
INSERT ... ON CONFLICT (client_id) DO NOTHINGfor idempotent sync. - Ktor endpoint — POST
/v1/syncaccepts a JSON array; validate JWT from the fleet auth service; process in aCoroutineScopeper request with timeout. - Conflict detection — if server
updated_atis newer than clientcapturedAt, return409with the canonical row for merge UI. - Observability — Micrometer metrics for batch size and conflict rate; structured logs with delivery ID and technician ID (no PII in message bodies).
- 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
| Need | Prefer Kotlin | Consider instead |
|---|---|---|
| New Android app (UI or KMP shared core) | Yes — official Google recommendation | Java only for legacy maintenance |
| JVM microservice with concise syntax | Yes — Ktor or Spring Boot + Kotlin | Java for teams without Kotlin experience |
| Shared mobile + server models (KMP) | Yes — common business logic module | Separate Swift/Kotlin with OpenAPI codegen |
| Smallest static binary CLI | No — JVM or Native still heavier than Go | Go or Rust |
| Browser-only front end | Rarely Kotlin/JS today | TypeScript / React |
| Hard real-time embedded | No | C, C++, Rust |
| Data science notebooks | No | Python |
| Large legacy Spring monolith in Java only | Gradual Kotlin modules | Stay 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 coroutines —
Thread.sleepor JDBC without R2DBC blocks the dispatcher; usewithContext(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
dataif the ORM requires mutable fields. - Overusing !! operator —
value!!throws at runtime like Java NPE; prefer safe calls or early returns. - Serializable schema drift —
kotlinx.serializationrequires 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
-Werroror 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
ApplicationStoppedhooks draining coroutine scopes. - Connection pools sized to Postgres limits; statement timeouts on JDBC paths.
- Secrets from environment or vault — never committed in
local.propertiesor 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
- Java fundamentals explained — JVM bytecode, collections, and interop baseline for Kotlin teams
- REST API design explained — versioning, idempotency, and error contracts for sync endpoints
- PostgreSQL fundamentals explained — persistence for Ktor and Spring Kotlin services
- Docker fundamentals explained — container images for JVM Kotlin microservices