Kotlin for Java Survivors: Life After Semicolons
A sixteen-part recovery program for Java developers moving to Kotlin: fewer NullPointerExceptions, zero semicolons, coroutines instead of thread pools, and two Spring Boot 4.1 services to prove it, one with an AI assistant that knows what it doesn't know. Just enough syntactic sugar to question why you suffered so long.
Kotlin for Java Survivors: Life After Semicolons
A 16-part series for senior Java and Spring developers learning Kotlin 2.4. Follow Kabir, a 14-year Java veteran, from culture shock to shipping two Spring Boot 4.1 services with Kafka, Qdrant, Spring AI and LangChain4j.
"Life After Semicolons" — Syntax, Control Flow and the Build
Kabir's first Kotlin pull request is Java with the semicolons left in. We set up a Kotlin 2.4 Gradle build, see what top-level functions compile to, and cover val/var, types without primitives, strings, default arguments, if/when/try as expressions, ranges and loops.
"The Billion-Dollar Fix" — Null Safety and the Type System
A NullPointerException from a Java supplier SDK crashes Kabir's Kotlin code. We cover nullable types and the safe-call operators, what they compile to, smart casts and their limits, platform types and JSpecify, lateinit vs lazy, Any/Unit/Nothing, and == vs ===.
"Classes Without Ceremony" — Constructors, Properties and Objects
Kabir starts typing getters and Lena stops him. We cover properties instead of fields, accessors and explicit backing fields, primary constructors and init order, final-by-default classes, Kotlin's different protected and internal, objects and companions instead of static, nested vs inner classes, and enums with entries.
"Data, Sealed and Value" — Modelling the Domain
Two swapped String arguments make a Pune till ring up toor dal at zero. We rebuild the catalog model with data classes and copy(), destructuring, sealed hierarchies with exhaustive when, value classes that cost nothing until they box, and type aliases, and see what each compiles to.
"Functions Are Values" — Lambdas, References and Inline
A return inside forEach silently skips half of a pricing audit. We look at what a Kotlin lambda really is: function types instead of functional interfaces, closures over mutable variables, references, fun interfaces, inline functions and where each kind of return goes, plus tailrec and a first look at lambdas with receivers.
"Extend, Scope, Delegate" — Kotlin's Power Tools
A region code and an audit log threaded through six layers of parameters start a design debate. We cover extension functions and their static dispatch, scope functions with a decision table, context parameters (Stable in 2.4), class and property delegation, operator overloading and infix calls, and build a type-safe product-query DSL with @DslMarker.
"Collections Without Collectors" — Collections, Sequences and Strings
A Java utility sorts a "read-only" Kotlin list in place, and a nightly job drowns the garbage collector in intermediate lists. We look at what Kotlin collections are on the JVM, the operations that replace Collectors, sequences versus eager chains versus Streams, arrays, strings and regex, and kotlin.time and Uuid.
"Generics Without Wildcards" — Variance, Reified and Constraints
A Java repository signature full of ? extends and ? super meets Kotlin, and most of the wildcards disappear. We cover declaration-site variance with out and in, use-site and star projections, bounds and where clauses, nullable type parameters and T & Any, erasure and @JvmName, reified type parameters, and build a typed event envelope, router and repository.
"Errors Without Checked Exceptions" — Failure Modelling, Annotations and Reflection
The Java pricing team can't catch an IOException that Kotlin throws. We cover Kotlin's answer to checked exceptions: @Throws for Java callers, try as an expression, require/check/error, Result and runCatching (and their traps), sealed error types, the Experimental unused-return-value checker, annotation use-site targets under the Kotlin 2.4 defaults, and reflection basics.
"Suspend Your Disbelief" — Coroutines Fundamentals
Cancelling an import leaves forty supplier calls running on virtual threads. We learn coroutines from the Java side: suspend functions and the state machine the compiler builds, launch/async/runBlocking, structured concurrency, coroutineScope vs supervisorScope, dispatchers and limitedParallelism, cooperative cancellation and timeouts, exception propagation, and how to debug it all.
"Flowing Data" — Flow, Channels and Virtual Threads
Store managers want live stock numbers, and Kabir reaches for Flux. We cover cold Flow and its operators, back-pressure with buffer, conflate and debounce, hot StateFlow and SharedFlow, channels, Reactor interop in both directions, testing with runTest and virtual time, and a decision table for virtual threads versus coroutines.
"Bilingual" — Java Interop, Build Plugins, Testing and Tooling
The Java team wraps Kabir's Kotlin library in a 140-line adapter, and he learns what his API looks like from the other side. We cover calling Java from Kotlin and Kotlin from Java (@JvmStatic, @JvmOverloads, @JvmField, @JvmName, @JvmRecord, @JvmExposeBoxed), the compiler plugins Spring and JPA need, compilerOptions, testing with JUnit, MockK, Kotest and power-assert, and code quality with ktlint, detekt and explicit API mode.
"The Catalog" — Spring Boot 4.1, JPA and REST in Kotlin
Kabir's first entity is a data class, and his first integration test dies with a StackOverflowError. We build product-catalog-service on Spring Boot 4.1 and Kotlin 2.4.20: entities that are not data classes, Spring Data with nullable returns, validation and Jackson 3 at the REST boundary, ProblemDetail errors, transactions that roll back on checked exceptions, the Gradle build, Testcontainers 2 and virtual threads.
"Reactive and Events" — Coroutines, Streaming and Kafka
The AI team needs every product change, and Lena asks who owns the event schema. We publish a sealed, kotlinx.serialization event hierarchy to Kafka after commit and stream it as server-sent events from a Flow. We fan out to suppliers with suspend controllers and a suspending @HttpExchange client, and keep the request id in the logs as coroutines hop threads, all on Spring MVC with virtual threads.
"The Assistant" — Spring AI 2.0, Qdrant and RAG
The prototype invented a return policy, and Kabir builds the service that can't. We build product-ai-service on Spring AI 2.0: a Kafka consumer that keeps a Qdrant index in step with the catalog, filtered semantic search, answers grounded in store policies, a tool for live prices, and a local model through Ollama, with the traps we hit on the way.
"The Second Opinion" — LangChain4j, Side by Side
The architecture review board asks what happens if Spring AI is the wrong bet, and Kabir answers with a second adapter. We implement the same ports with LangChain4j behind a profile, rebuild its index by replaying Kafka, run one test suite against both libraries, look at where Kotlin meets two Java APIs, and compare the libraries honestly. Then we wrap up the series.