Series Posts

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 0: 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.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 1: "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.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 2: "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 ===.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 3: "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.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 4: "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.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 5: "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.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 6: "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.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 7: "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.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 8: "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.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 9: "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.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 10: "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.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 11: "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.

Conceptual Kotlin for Java Survivors: Life After Semicolons

Part 12: "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.

Project Kotlin for Java Survivors: Life After Semicolons

Part 13: "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.

Project Kotlin for Java Survivors: Life After Semicolons

Part 14: "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.

Project Kotlin for Java Survivors: Life After Semicolons

Part 15: "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.

Project Kotlin for Java Survivors: Life After Semicolons

Part 16: "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.