Kotlin logo

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:

  1. Getting Started With Kotlin for Java Developers
  2. Evaluating Kotlin in Real Projects
  3. Growing Kotlin Adoption in Your Company
  4. Helping Decision-Makers Say Yes to Kotlin
  5. 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 MutableList instead 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:

  1. Comprehensive testing coverage: Since LLMs are unpredictable, you need reliable tests to catch AI hallucinations.
  2. 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.
  3. 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:

Meta

Uber’s approach:

Uber

  • 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.

Kotlin Heroes

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.

Urs Peter

Urs is a seasoned software engineer, solution architect, conference speaker, and trainer with over 20 years of experience in building resilient, scalable, and mission-critical systems, mostly involving Kotlin and Scala.

Besides his job as a consultant, he is also a passionate trainer and author of a great variety of courses ranging from language courses for Kotlin and Scala to architectural trainings such as Microservices and Event-Driven Architectures.

As a people person by nature, he loves to share knowledge and inspire and get inspired by peers on meetups and conferences. Urs is a JetBrains certified Kotlin trainer.

image description