Build & Compiler
Between Kotlin source and a finished artifact stands a pipeline, not a single button. The compiler goes first: its frontend parses the text, resolves names and types, checks the program and builds a semantic model, while the backend lowers that model into JVM bytecode, native code or JS. On top of the compiler, Gradle orchestrates the build — it splits the project into modules, caches and recompiles only what changed, and along the way runs the annotation processors (kapt or KSP) that generate code. And only at the very end, on Android, packaging steps are applied to the bytecode: assembling the required build variant and running R8, which shrinks and obfuscates the release. Build speed and release correctness are born on different floors of this pipeline — and the traps there differ too.
The main dividing lines are worth naming up front. K2 is a frontend — not a backend and not a Gradle plugin; it became the default and stable in Kotlin 2.0, and version 2.4 removed K1 entirely, so there is nothing left to "enable K2 with a flag" for — there is no choice. Annotation processing happens at compile time, not at runtime through reflection, and kapt, with its Java stubs, is a dead-end branch next to KSP. Incremental build speed lives at module boundaries: it is not any edit that triggers a recompile but an ABI change, and api versus implementation decides whether that change cascades. And the bugs that are most expensive to debug are the ones that exist only in release: they are almost always left behind by R8, which stripped code that only reflection reaches. The layer-by-layer breakdown is below.
Topic map
- The K2 frontend — what the compiler's frontend does versus the backend, what FIR is, and why K2 became the default and stable in Kotlin 2.0.
- A single frontend and migration — one analysis for every backend, why K2 is stricter than the old frontend, what had to be rewritten in the plugins, and how 2.4 removed K1.
- kapt versus KSP — annotation processing as compile-time code generation,
kapt's Java stubs versusKSPreading symbols directly. - The Gradle Kotlin DSL — compiled build scripts with typed accessors, convention plugins versus copy-paste, and the version catalog.
- Modules and build speed — why splitting into modules speeds the build up, ABI versus a function body, and
apiversusimplementationas the main lever. - Build variants — a variant as the product of a build type and a product flavor, each with its own source set, and the explosion in the number of combinations.
- R8: shrinking and obfuscation — tree-shaking, optimization, obfuscation and resources in a single pass; the classic reflection trap and keep rules.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Swapping the roles: the frontend generates the code, the backend checks the types | The other way round: the frontend resolves names and types, the backend lowers the result into bytecode/native code/JS |
| Calling K2 an experimental preview behind a flag | K2 has been stable and the default since 2.0, and 2.4 removed K1 — there is nothing to enable |
| Treating K2 as a backend or a Gradle plugin | K2 is the rewritten frontend; that is what was unified, not the backends |
| Expecting a frontend switch to be source-compatible | K2 is stricter: latent smart-cast and nullability errors that K1 let through now surface |
| Thinking a K1 plugin will simply keep working under K2 | Plugins were rewritten against the FIR API; an unported plugin does not run under K2 |
| Thinking annotation processing happens at runtime through reflection | It is compile-time code generation: the processor reads declarations and writes new sources |
Assuming a kapt processor will work under KSP unchanged | They are different APIs; the move is per-library, and each library must ship a KSP processor |
Crediting KSP's speedup to parallelism | It comes from dropping the Java-stub pass and javac, not from running the same processors in parallel |
| Selling the Kotlin DSL as a faster build | It gives you typing and refactoring, while a cold build is even slightly slower because the scripts are compiled |
| Expecting the DSL itself to remove duplication | Deduplication comes from a convention plugin, not from moving to .kts |
| Thinking any edit recompiles the whole module chain | A body recompiles almost nothing; only an ABI change starts a cascade |
| Thinking a body edit is expensive and a signature change is cheap | It is the other way round: a body is local, while a signature is ABI, and everything depending on it is rebuilt |
Exposing everything through api so the modules can see each other | api leaks into consumers' compile classpath, and the change cascades again — use implementation |
| Cutting the monolith by layers rather than by features | An edit to one feature still touches every layer module — there is no incrementality gain |
| Treating a build variant as a runtime flag inside a single artifact | A variant is a separate build with its own source set: build type × product flavor |
Thinking R8 only compresses the archive or encrypts the bytecode | It does tree-shaking, optimization, renaming and resource shrinking — code is removed and renamed |
Believing R8 will not touch a class because reflection reaches it at runtime | Without a keep rule a statically unreachable class is stripped; the fix is @Keep, not isMinifyEnabled = false |
Interview relevance
The build toolchain is the section where the interviewer probes not your vocabulary but your model of the pipeline: which stage does what at which moment, and what exactly breaks its caching. The classic dividing question is "what is K2": the right answer is not "a new version" but "the rewritten compiler frontend, stable and default since 2.0, the only one after 2.4". The next level is practical: why the build is slow (module boundaries and ABI, not the line count) and why release fails while debug does not (R8, which only runs there). A candidate who fixes those symptoms by turning minification off or by handing the Gradle daemon more heap does not understand what is happening under the hood.
Typical checks:
- The roles of the frontend and the backend, and that K2 is the frontend — stable since 2.0, with K1 removed in 2.4.
- Why one shared frontend matters for multiplatform, and what had to be rewritten in the plugins.
- Why
KSPis faster thankapt— that the speedup comes from dropping the Java stubs, not from parallelism. - What the Kotlin DSL actually gives you, and where deduplication comes from a convention plugin rather than the DSL itself.
- What devalues an incremental build: ABI versus a body,
apiversusimplementation, and cutting by features. - What a build variant is, and that it has its own source set.
- What
R8does to a release, and why a reflective model needs a keep rule.
Common wrong answer: "K2 is a fast new backend you turn on with a flag to speed the build up." That is three errors in one sentence. What was unified is the frontend, not the backends; K2 is not "turned on" — it is the default since 2.0 and the only one since 2.4; and it is not about build speed but about a single analysis and a stricter check — and because it is stricter, migration means fixing new errors, not a free speedup. The same candidate usually believes a K1 plugin will survive the upgrade on its own — while a plugin not ported to FIR simply will not run under K2.