Kotlin
A concise multiplatform language developed by JetBrains
Scaling Kotlin Adoption Across Your Organization
Guest post by Urs Peter, Senior Software Engineer and JetBrains-certified Kotlin Trainer. For readers who’d like a more structured way to build Kotlin skills, Urs also leads the Kotlin Upskill Program at Xebia Academy.
This is the fifth post in The Ultimate Guide to Successfully Adopting Kotlin in a Java-Dominated Environment, a series that follows how Kotlin adoption grows among real teams, from a single developer’s curiosity to company-wide transformation.
All parts in the series:
- Getting Started With Kotlin for Java Developers
- Evaluating Kotlin in Real Projects
- Growing Kotlin Adoption in Your Company
- Helping Decision-Makers Say Yes to Kotlin
- Scaling Kotlin Adoption Across Your Organization
Success Factors for Large-Scale Kotlin Adoption
Gaining developer buy-in and management support for Kotlin is a significant milestone, but it’s not the finish line. The real challenge begins when you’re faced with existing Java codebases that need to coexist with or transition to Kotlin. How do you navigate this hybrid world effectively?
The key to managing legacy codebases is developing a strategy that aligns with your organizational goals and operational realities. Here’s a proven approach that has worked out well in practice.
Application lifecycle strategy
Different applications require different approaches based on their lifecycle phase. Let’s examine three distinct categories:
End-of-life applications
Strategy: Leave them alone.
If an application is scheduled for retirement, there’s no business case for migration. Keep these systems in Java and focus your energy where it matters most. The cost of migration will never be justified by the remaining lifespan of the application.
New systems
Strategy: Default to Kotlin.
In organizations where Kotlin adoption is complete, greenfield projects naturally start with Kotlin. If the adoption process is in progress, teams often get the choice between Java and Kotlin. Choose wisely ;-).
Active applications
Strategy: Pragmatic, feature-driven migration.
Active applications are the ones that require careful consideration: Rewriting for the sake of rewriting is a tough sell to your Product Owner. Instead, combine migration efforts with new feature development. This approach provides tangible business value while modernizing your codebase. The different attack angles we already discussed in the section: Extend/Convert an existing Java application.
Java-to-Kotlin conversion approaches
When converting Java to Kotlin, you have several options, each with distinct trade-offs:
1. Complete rewrite
Best for: Small codebases
Challenge: Time-intensive for larger systems
Rewriting a codebase from scratch gives you the cleanest, most idiomatic Kotlin code. This approach is suitable for small codebases, like a MicroService. For large codebases, this approach generally tends to be prohibitively expensive.
2. IDE auto-conversion with manual refinement
Best for: Medium codebases with dedicated refactoring time
Challenge: Manual refinements are mandatory
IntelliJ IDEA’s Convert Java to Kotlin feature provides a literal translation that’s far from idiomatic. Consider this example:
Take 1:
Java
record Developer(
String name,
List<String> languages,
String name,
) {}
Raw auto-conversion result:
Kotlin
@JvmRecord data class Developer( val name: String?, val languages: MutableList<String?>? )
This conversion has several issues:
- Everything becomes nullable (overly defensive).
- Java Collections become Kotlin’s
MutableListinstead of Kotlin’s default read-only List.
Improving the conversion with jspecify annotations:
Luckily, for the conversion of all Java types to Nullable types in Kotlin, there is a remedy in the form of @NonNull/@Nullable annotations. Different options are available; the most modern one is jspecify, which has recently also been officially supported by Spring:
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
implementation("org.jspecify:jspecify:1.0.0")
With jspecify, we can annotate the Java code with @Nullable and @NonNull.
Take 2:
Java
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
record Developer(
@NonNull String name,
@NonNull List<@NonNull String> languages,
@Nullable String email
) {}
Now the auto-conversion produces much better results:
Kotlin
@JvmRecord data class Developer( //😃 non-null as requested val name: String, //😃 both, collection and type are non-null val languages: MutableList<String>, //😃 nullable as requested val email: String?, )
Limitations of the auto conversion approach:
Even with jspecify annotations, complex Java patterns don’t translate well. Consider this auto-converted code example shown in section: 3. No more checked Exceptions, yet safer code in Kotlin:
Kotlin
fun downloadAndGetLargestFile(urls: MutableList<String>): String? {
//🤨 using Stream, instead of Kotlin Collections
val contents = urls.stream().map { urlStr: String? ->
//🤨 still using Optional, rather than Nullable types
//🤨 var but we want val!
var optional: Optional<URL>
//🤨 useless try-catch, no need to catch in Kotlin
try {
optional = Optional.of(URI(urlStr).toURL())
} catch (e: URISyntaxException) {
optional = Optional.empty<URL>()
} catch (e: MalformedURLException) {
optional = Optional.empty<URL>()
}
optional
}.filter { it!!.isPresent() }//🤨 discouraged !! to force conversion to non-null
.map {it.get() }
.map { url: URL ->
//🤨 useless try-catch, no need to catch in Kotlin
try {
url.openStream().use { `is` ->
String(`is`.readAllBytes(), StandardCharsets.UTF_8)
}
} catch (e: IOException) {
throw IllegalArgumentException(e)
}
}.toList()
//🤨 usage of Java collections…
return Collections.max(contents)
}
The autoconversion is far away from the desired, idiomatic result:
Kotlin
fun downloadAndGetLargestFile(urls: List<String>): String? =
urls.mapNotNull {
runCatching { URI(it).toURL() }.getOrNull()
}.maxOfOrNull{ it.openStream().use{ it.reader().readText() } }
The auto-conversion provides a starting point, but significant manual refinement and knowledge of idiomatic Kotlin is required to achieve truly idiomatic Kotlin.
3. AI-assisted conversion
Best for: Larger codebases with robust testing infrastructure
Challenge: Manual review required
AI can potentially produce more idiomatic results than basic auto-conversion, but success requires careful preparation:
Prerequisites:
- Comprehensive testing coverage: Since LLMs are unpredictable, you need reliable tests to catch AI hallucinations.
- Well-crafted system prompts: Create detailed instructions for idiomatic Kotlin conversions that are aligned with your standards. You can use this system prompt as a starting point.
- Extensive code review: AI output requires a thorough review for logical and idiomatic correctness, which can be mentally taxing for large codebases.
Using the proposed system prompt to guide the conversion, the result is quite satisfying, yet not perfect:
Kotlin
fun downloadAndGetLargestFile(urls: List<String>): String? {
val contents = urls
.mapNotNull {
urlStr -> runCatching { URI(urlStr).toURL() }.getOrNull()
}.mapNotNull { url -> runCatching {
url.openStream().use { it.readAllBytes().toString(UTF_8)
}
}.getOrNull() }
return contents.maxOrNull()
}
4. Auto-conversion at scale
Best for: Massive codebases requiring systematic transformation
Currently, there’s no official tooling for converting Java codebases to idiomatic Kotlin at scale. However, both Meta and Uber have successfully tackled this challenge for their Android codebases using approaches that work equally well for backend applications. The following documentation and talks provide insights into how Meta and Uber approach this quest:
Meta’s approach:

- Strategy: Rule-based, deterministic transformation
- Resources:
Uber’s approach:

- Strategy: Rule-based, deterministic transformation using AI to generate conversion rules
- Resource: KotlinConf presentation
Both companies succeeded by creating systematic, repeatable processes rather than relying on manual conversion or simple automation. Their rule-based approaches ensure consistency and quality across millions of lines of code.
Important: Converting Java to Kotlin at scale introduces a challenge on the social level: to be reliable, you still want ‘the human in the loop’ reviewing the generated code. However, if not planned carefully, Engineers can easily be overwhelmed by the flood of pull requests resulting from the automated conversion. Therefore, the social impact needs to be considered carefully.
Remember, successful large-scale Kotlin adoption isn’t just about converting code – it’s about building team expertise, establishing coding standards, and creating sustainable processes that deliver long-term value to your organization.
Short recap on when to use which approach:
- Small application or being rewritten anyway?
→ Rewrite in Kotlin (1) - Medium codebases with dedicated refactoring time or team learning Kotlin?
→ Intellij IDEA Auto-Conversion + refine (start with tests first) (2) - Mid/large code with repeated patterns and good tests?
→ AI-assisted approach (3) - Org-level migration to Kotlin across many services?
→ Auto-Conversion at scale with concrete plan (platform-led) (4)
Unlock Kotlin’s full potential
Kotlin makes developers productive fast. Its concise syntax, safety features, and rich standard library help many developers write better code within weeks – often through self-study or on-the-job learning. But without guidance, many fall into the trap of using Kotlin in a Java-like way: sticking to mutable structures, verbose patterns, and missing out on idiomatic features.
Reaching the next level
Reaching the next level – embracing immutability, expression-oriented code, DSLs, and structured concurrency with Coroutines (with or without Virtual Threads) – is where many developers get stuck.
At this stage, I’ve found that external training makes a far greater difference than self-study. Even developers with years of Kotlin experience often benefit from focused coaching to adopt idiomatic patterns and unlock the language’s full potential.

To Kotlin or Not to Kotlin: What Kind of Company Do You Want to Be?
Ultimately, the decision to adopt Kotlin reflects your engineering culture. Do you value the:
- Traditional approach (Java): Conservative, ceremonial, stable.
- Progressive approach (Kotlin): Pragmatic, modern, adaptive.
Both have their merits. Java will get the job done. Kotlin will likely get it done better, with happier developers and fewer bugs. The question isn’t whether Kotlin is better – it’s whether your organization is ready to invest in being better.
The journey from that first Kotlin test to organization-wide adoption isn’t always smooth, but with the right approach, it’s remarkably predictable. Start small, prove value, build community, and scale thoughtfully.
Your developers – current and future – will thank you.