The Window Closes: Why I Switched to Kotlin Before the Codebase Got Too Big
Switching from Java to Kotlin is easy while the codebase is small; delay it, and the same migration can turn from a one-week job into quarters of engineering work.
I moved a project from Java to Kotlin in a single pass, back when the codebase was small enough that doing so cost almost nothing. The system I work on now is a different animal: hundreds of thousands of lines of Java, a monolith rather than a set of services. Converting it wholesale is the kind of gamble nobody signs off on. The option still exists technically, but the cost is now measured in quarters of engineering time rather than days. How much code existed when someone first asked about Kotlin decided everything that followed.
I have spent more than a decade building and scaling high-load distributed systems on the JVM, across fintech, security, and media infrastructure, and the pattern holds: you pay for a language migration at the start, well before anyone puts it on a roadmap.
The Easy Migration
The small project was a handful of Spring Boot microservices with JPA and PostgreSQL, still short of production at a few thousand lines of code. A colleague who spent his evenings in the Kotlin community suggested we try it on this one. The team was small and curious, so we agreed within a day or two. Spring compatibility was the thing we worried about, and it never became a problem.
Because nothing had shipped yet, we converted the entire codebase at once. IntelliJ IDEA can rewrite a Java file to Kotlin with a single keystroke, and the converter handles about 80 percent of the conversion on its own. The rest of the week went into manual cleanup. Adding nullability annotations stripped out the platform types, and the compiler immediately flagged a dozen possible null-pointer exceptions that Java had been hiding inside Optional. We moved from field injection to constructor injection, which fits Kotlin far better. Data classes replaced our POJOs and trimmed about a quarter of the boilerplate, making the model easier to read.
Static members needed more attention. After conversion, our public static final constants ended up inside a companion object, which changed the bytecode that Java callers expected, so we annotated them with @JvmStatic and @JvmField to keep the modules we hadn't touched yet compiling. By the end of the week, the tests were green, and Spring Boot loaded the new classes without complaint. The whole exercise took one week because the codebase was small, which is the entire point: the same cleanup across a large system is measured in quarters, not days.
The Parser That Sold Me
The message parser is what convinced me the move was worth defending to the rest of the team. Our services received data, validated it against an Avro schema, and returned a result. Parsing under load burns resources, and many Java libraries rely on runtime reflection during serialization and deserialization. Jackson can match the performance without reflection, to be fair: its Afterburner and Blackbird modules generate bytecode at runtime for field and method access, which is a real optimization. The cost shows up per format. JSON, XML, YAML, TOML, and Avro protocols each want their own modules and manual wiring, and a new format usually means finding a library or writing one.
Kotlinx.serialization works differently. Its compiler plugin generates every serialization routine at build time and never calls reflection. We standardized most formats around Kotlinx.serialization, using it as the common model layer for JSON, XML, YAML, and TOML, then bolted on Avro as a separate plugin module. The schemas were plain Kotlin classes with the @Serializable annotation, and code generation handled the rest. We had to implement Avro's default values in the plugin by hand, but only once. A smaller dependency surface, typing that held end to end, one approach instead of five.
That early switch also bought time to question the rest of the stack while it stayed light: jOOQ for queries that grew complicated, smaller web frameworks for lightweight services, and test tooling with more expressive assertions. Experiments are cheap on a compact codebase, and you can settle choices that would turn into prolonged arguments once the system has weight and a dozen people depending on it.
The Monolith, and What to Do About Yours
The same migration on the monolith I work on now is a different proposition, and I would push back on anyone who claimed it could happen in one pass. The window there has thinned to a slot rather than shut. Java and Kotlin still compile side by side in a single project, leaving an incremental approach open. You can write new tests in Kotlin against the existing Java, peel bounded contexts off into separate Kotlin services, and drop the idea that migration has to happen all at once. None of that demands the big-bang rewrite that scares management off, and each piece keeps the window from closing further.
Postponing the conversation makes it more expensive every quarter, which is the cost the easy migration above never had to pay. If your codebase is still small, bring it up now, while the call is for engineers, not a budget committee. The migration window rarely closes dramatically. One quarter, it simply stops being practical. It just gets heavier to lift, and in one planning session, you realize the topic stopped coming up a while ago.
Opinions expressed by DZone contributors are their own.