skip to content

Kotlin libraries often prefer compile-time codegen (KSP / compiler plugins) over runtime reflection. What are the engineering trade-offs of that choice?

level: seniorimportance: should knowfreq 35%

answer

  1. Codegen: fast runtime, fail-fast, shrink/native friendly
  2. Reflection: flexible, zero build step, slower + shrink-hostile
  3. Native/GraalVM have limited reflection -> codegen wins
  4. Codegen cost = build complexity + less runtime flexibility
  5. Moshi/Room/Dagger codegen; serialization is a compiler plugin

basics

~20 s

Generating code while building makes the program start faster, run faster, and catch mistakes early because everything is decided at compile time. Reflection is more flexible and needs no extra tooling, but is slower at runtime and harder to optimize, especially for native or small binaries.

solid answer

~50 s

Compile-time codegen (KSP processors, compiler plugins) resolves the work before the program runs: it produces concrete generated code, so there is no per-call reflection cost, errors surface at build time, and the output is friendly to ahead-of-time compilation, R8/ProGuard shrinking, GraalVM native image, and Kotlin/Native (which has limited reflection). The costs: longer/more complex builds, extra Gradle plugins, generated sources to reason about, and less runtime flexibility. Runtime reflection (kotlin-reflect, java.lang.reflect) needs no build step and adapts to types known only at runtime, but pays per-invocation cost, defeats dead-code elimination (harder to shrink), can break under obfuscation, and is partially unsupported on Native. That is why Moshi/Room/Dagger favor codegen and why serialization is a compiler plugin. The principled choice: codegen for hot paths, native targets, and fail-fast guarantees; reflection for highly dynamic or rarely-hit code where build simplicity matters more.

code

kotlin · 6 lines
kotlin
@JsonClass(generateAdapter = true) // KSP generates UserJsonAdapter at build time
data class User(val id: Long, val name: String)

// vs reflective approach (needs kotlin-reflect):
val kClass = User::class
kClass.members.forEach { /* resolved at runtime - slower, shrink-hostile */ }

go deeper

for a junior

Knows codegen happens at build time and is generally faster at runtime than reflection.

for a middle

Lists key trade-offs: runtime speed and fail-fast for codegen vs flexibility and simplicity for reflection.

for a senior

Connects codegen to shrinking, AOT/GraalVM, Kotlin/Native limits, and explains when reflection is still the right call.

for a principal

Frames it as moving cost/errors left, sets policy per target platform and performance budget, and weighs build-tooling maintenance against runtime gains.

## The two strategies Many frameworks need to do per-type 'meta' work — serialize a class, build a DB mapping, wire dependencies. There are two ways to get the metadata: - **Runtime reflection** — inspect types *while the program runs* (`kotlin-reflect`'s `KClass`, or Java's `java.lang.reflect`). Decisions happen on first use. - **Compile-time codegen** — figure it out *during the build* via **KSP** processors or **compiler plugins**, emitting concrete generated code (e.g. a hand-written-looking serializer). ## Why codegen is often preferred in Kotlin - **Runtime performance** — no reflective lookups on the hot path; generated code is plain method calls the JIT optimizes well. - **Startup time** — nothing to introspect at boot. - **Fail-fast** — missing/incompatible mappings become **build errors**, not runtime exceptions in production. - **Shrinking & dead-code elimination** — tools like **R8/ProGuard** can prove what's used; reflection forces broad keep-rules. - **Native / AOT friendliness** — **GraalVM native image** and **Kotlin/Native** have limited or costly reflection; codegen sidesteps that (Native has only minimal reflection support). ## The costs of codegen - **Build complexity** — extra Gradle plugins (KSP, compiler plugins), longer builds, generated sources in your `build/` output to understand and debug. - **Less runtime flexibility** — you cannot adapt to types discovered only at runtime; everything must be known at compile time. - **Tooling coupling** — compiler plugins track Kotlin versions; KSP processors must exist for your library. ## When reflection still wins - Highly **dynamic** scenarios (plugins, scripting, generic frameworks handling arbitrary user types). - **Cold / rarely executed** code where per-call cost is irrelevant. - When you want **zero build tooling** and maximum simplicity. ## Concrete examples ```kotlin // Codegen path: Moshi generates an adapter at build time (KSP) @JsonClass(generateAdapter = true) data class User(val id: Long, val name: String) // -> UserJsonAdapter is generated; no reflection at runtime // Reflection path (Moshi can also do this) needs kotlin-reflect on the classpath // and resolves the structure on first use - simpler build, slower & shrink-hostile. ``` Room and Dagger/Hilt also generate code; **kotlinx.serialization** is a compiler plugin precisely to avoid reflective serialization. ## Decision framework - Hot paths, native/AOT targets, or fail-fast needs -> **codegen**. - Dynamic, cold, or build-simplicity-first code -> **reflection** is acceptable. - Either way, understand that codegen moves cost and error detection **left** (to build time).

  • Why is reflection problematic for R8/ProGuard or GraalVM native image?
    Reflective access is opaque to static analysis, so shrinkers must keep extra classes/members (or fail). GraalVM/Native need explicit reflection config or have limited support, whereas generated code is plain references the tools can trace.
  • Does codegen eliminate all runtime cost?
    It removes reflective lookups, but the generated code still executes. The win is replacing per-call introspection with straight-line method calls the JIT/AOT compiler can optimize.

Codegen is prepping ingredients before the dinner rush; reflection is chopping to order while guests wait.

saying these in an interview costs you the question

  • Claiming reflection has no runtime cost
  • Ignoring that codegen adds build complexity and longer builds
  • Not knowing reflection hurts shrinking and native/AOT targets
  • Saying codegen and reflection are equivalent in capability for dynamic types
  • Assuming Kotlin/Native has full reflection support

context