Translating Java to Kotlin at Scale
How Meta built the Kotlinator — a six-phase automated pipeline around a headless J2K — to translate tens of millions of lines of Java to Kotlin without blocking their Android engineers or requiring them to click a button 100,000 times.

The conventional approach to a Java-to-Kotlin migration is incremental: write new code in Kotlin, leave existing Java alone, gradually translate the important files. Meta decided that leaving Java in place would cap the benefits of Kotlin at a fraction of what full conversion could deliver. Then they had to figure out how to actually do it.
Source
Translating Java to Kotlin at Scale
By: Meta
Published: Dec 18 2024
Type: Engineering article
Saved for the engineering detail on the Kotlinator pipeline and specifically for the argument about null safety that most migration articles skip: any remaining Java code is an active source of nullability problems for the Kotlin code around it, not a neutral baseline. The headless J2K implementation and the pre/postprocessing architecture are also worth studying as a pattern for large-scale automated code transformation.
The Problem With Stopping Halfway
Meta’s Android codebase went Kotlin-first in 2020. New code was written in Kotlin; existing Java code was left where it was. This is where most companies stop, and it produces a reasonable outcome: new features benefit from Kotlin’s null safety and more expressive syntax, while the existing Java foundation stays stable. The problem is that “reasonable” undersells what Kotlin can do when it is the only language in the codebase.
The argument Meta makes — and it is a careful one — is that residual Java code is not neutral. Any Java file that is not null-annotated, which describes most production Java code at scale, is what they call “an agent of nullability chaos.” When Kotlin code calls a Java method that returns a value, the Kotlin compiler does not know whether that value can be null. It has to trust annotations, and if those annotations are missing or wrong, the null safety guarantees Kotlin provides break down at that call site. The closer that Java code is to the center of the dependency graph — which is usually where the oldest, most reused, most untested code lives — the more Kotlin code inherits its nullability ambiguity. Full conversion removes that ambiguity at the source rather than papering over it with null checks throughout the Kotlin codebase.
There is also the build speed problem. Compiling Java is fast. Compiling Kotlin is slower. Compiling both together — the mixed-codebase state — is the slowest of all, because the compiler has to run multiple passes across two languages. As long as substantial Java remains, Meta needs to maintain parallel toolchains, which adds infrastructure overhead and cognitive load for the engineers who have to understand both.
The practical barrier to full conversion was never conceptual — Kotlin’s own tooling includes a Java-to-Kotlin converter (J2K) built into IntelliJ. The barrier was mechanical: Meta’s Android codebase contains roughly ten million lines of Java spread across close to 100,000 files. Running J2K on each file requires opening IntelliJ, navigating to the file, clicking the convert button, waiting a couple of minutes, reviewing the output, fixing build errors, and committing the result. Doing that at scale with human engineers is not a plan; it is a fantasy. The engineering investment required was not a better migration strategy but a tool that could run the conversion unattended.
That tool is the Kotlinator, built around a headless version of J2K. Making J2K headless was the first hard problem. J2K is deeply coupled to the IntelliJ IDE — it was designed to run inside a running IDE instance, with access to all the IDE’s symbol resolution and type information infrastructure. Running it on a remote server required creating an IntelliJ plugin that extends ApplicationStarter and calls directly into the JavaToKotlinConverter class. After working with JetBrains’ J2K expert Ilya Kirillov, the team settled on an approach similar to a headless inspection — close enough to the IDE’s internals to get correct symbol resolution, but runnable on a server without a display. The result converts multiple files in parallel on a remote machine, with a typical full conversion taking around 30 minutes per run. Individual engineers are completely unblocked: their local IDE is never occupied by a conversion job.
The Kotlinator runs six phases in sequence. First, a deep build of the code about to be converted, so that J2K has complete symbol resolution including third-party dependencies and generated code. Second, preprocessing — about 50 transformation steps that handle nullability annotations, J2K-specific workarounds, and changes required by Meta’s custom dependency injection framework. Third, the headless J2K conversion itself. Fourth, postprocessing — about 150 transformation steps that make the resulting Kotlin more idiomatic, handle Android-specific patterns, and apply further nullability fixes. Fifth, running linters with autofixes, so that patterns fixable by a lint rule are fixed consistently across every conversion rather than left to code reviewers. Sixth, a build-error-based repair phase: the Kotlinator builds the just-converted code, parses any errors, and applies targeted fixes — adding missing imports, inserting !! where the type system demands it, and similar mechanical repairs.
The pre- and postprocessing phases run on top of an internal metaprogramming tool called Editus, which uses JetBrains’ PSI libraries for both Java and Kotlin. The key property of Editus is that it can analyze broken code — code that does not compile — which is essential for postprocessing, where the converted Kotlin may have compilation errors that the system needs to reason about in order to fix them. Some postprocessing steps examine the converted file’s dependents to update them: if an interface method becomes a property after conversion, the classes that implement that interface need their method overrides rewritten as property overrides. Editus can do that analysis across thousands of unbuildable files at once, which would be impossible with a standard compiler plugin.
The deployment mechanism for converted diffs uses an internal cron-based automation system that produces a daily batch of conversion diffs (pull requests) based on a priority queue of files to convert. The system handles reviewer selection, runs tests and validations, and ships the diff once a human approves it. Human review is preserved; human initiation is removed. The bottleneck was always the ratio of engineer-hours to files-to-convert, and the Kotlinator solves that by making the engineer’s role purely review rather than also conversion.
My take
The framing that most migration articles use is “how do we get engineers to write Kotlin?” Meta’s framing is different: “how do we get the codebase into Kotlin without engineers having to care about the conversion at all?” That shift produces a completely different solution. The first framing leads to style guides, training courses, and migration guidelines. The second framing leads to a six-phase automated pipeline with headless IDE integration. The second approach scales to ten million lines because it removes humans from the slow path while keeping them in the quality-control path. The insight that the conversion job is mechanical — reviewable by a human but not initiated by one — is the key design decision, and it is reusable for any large-scale codebase transformation that has a good automated starting point.
Where this is useful
- Planning a language migration for a large Android codebase where incremental adoption has hit its ceiling
- Designing automated code transformation pipelines where the transformation tool exists but does not scale to the required volume
- Evaluating the true cost of a mixed-language codebase (build speed, toolchain overhead, null safety leakage)
- Not applicable for small codebases where the overhead of building automation infrastructure exceeds the cost of manual conversion — below roughly a few thousand files, clicking the button is faster than building the Kotlinator
Apply this: Before planning a large codebase migration, calculate how many times a human would have to manually perform the transformation — if it exceeds a few hundred, build the automation first.
What I want to remember
Remember: In a mixed Java/Kotlin codebase, every unnull-annotated Java file is an active nullability problem for the Kotlin code that calls it, not a neutral legacy — full conversion is the only way to get the full null-safety guarantee.
Related reads
- Shopify’s React Native migration used the same principle — committing fully to the new approach rather than hedging with a mixed codebase
- Meta’s Rust mobile transition applies the same automation-first thinking to a different language migration problem
- How Meta’s typed Python survey reveals the same mixed-types cost pattern at the Python layer of their stack