skip to content

Why does Kotlin recommend KSP over kapt for annotation processing, and how do they differ mechanically?

level: middleimportance: must knowfreq 65%

answer

  1. kapt = Java stubs for JSR-269 processors, slow
  2. KSP = SymbolProcessor over KSClassDeclaration, ~2x faster
  3. Both are compile-time codegen, not runtime reflection
  4. kapt is maintenance mode; KSP is the future
  5. Apply via ksp(...) configuration + KSP Gradle plugin

basics

~20 s

Both generate code at build time from annotations. kapt fakes Java stubs so old Java annotation processors work, which is slow. KSP reads Kotlin code directly through a lightweight API, so it is much faster and understands Kotlin features properly.

solid answer

~40 s

kapt (Kotlin Annotation Processing Tool) lets existing javax.annotation.processing (JSR-269) processors run against Kotlin. To do that it generates Java stub classes for your Kotlin code, then runs javac-style processors over them — the stub generation is the expensive step and it loses Kotlin-specific information. KSP (Kotlin Symbol Processing) is a purpose-built API: processors implement SymbolProcessor / SymbolProcessorProvider and walk Kotlin symbols (KSClassDeclaration, KSFunctionDeclaration) directly, with no Java stubs. KSP is typically ~2x faster, models Kotlin natively (nullability, suspend, extension functions, properties), and is the strategic direction (kapt is in maintenance mode). Both produce generated sources at compile time — not runtime reflection. Libraries like Room, Moshi, and Dagger/Hilt ship KSP processors. You apply KSP via the com.google.devtools.ksp Gradle plugin and add the processor with the ksp(...) configuration.

code

kotlin · 10 lines
kotlin
// build.gradle.kts
plugins {
    kotlin("jvm") version "2.1.0"
    id("com.google.devtools.ksp") version "2.1.0-1.0.29"
}

dependencies {
    implementation("androidx.room:room-runtime:2.6.1")
    ksp("androidx.room:room-compiler:2.6.1") // KSP processor, not kapt
}

go deeper

for a junior

Knows both generate code at build time from annotations and that KSP is the newer, faster option.

for a middle

Explains kapt's Java-stub mechanism vs KSP's native SymbolProcessor API and why KSP is faster; applies the ksp() configuration.

for a senior

Discusses Kotlin-native modeling (nullability, suspend), maintenance-mode status of kapt, migration trade-offs, and that neither mutates existing code.

for a principal

Weighs ecosystem migration risk, build-time impact across a large multi-module build, and the boundary between KSP (generate) and compiler plugins (transform).

## What annotation processing is **Annotation processing** = generating extra source/class files at **build time** by reading annotations (e.g. `@Entity`, `@JsonClass`) in your code. It is **compile-time codegen**, the opposite of doing the same work via **runtime reflection**. Generating code up front means less work and fewer surprises at runtime, and it is friendlier to ahead-of-time/native compilation. ## kapt — the compatibility bridge **kapt** (Kotlin Annotation Processing Tool) exists so the huge ecosystem of **Java** annotation processors (the standard `javax.annotation.processing` / **JSR-269** API, e.g. `AbstractProcessor`) can run on Kotlin code. Java processors only understand Java type elements, so kapt: 1. Generates **Java stub** files representing your Kotlin declarations (signatures only). 2. Runs the Java processors against those stubs. 3. Feeds generated sources back into compilation. The **stub generation** step is the bottleneck — it effectively compiles your code twice and loses Kotlin-specific detail (it sees a Java-shaped view). kapt is now in **maintenance mode**. ## KSP — the Kotlin-native API **KSP** (Kotlin Symbol Processing) is a dedicated API. A processor implements: ```kotlin class MyProcessor( private val codeGenerator: CodeGenerator, private val logger: KSPLogger, ) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { val symbols = resolver.getSymbolsWithAnnotation("com.example.MyAnno") symbols.filterIsInstance<KSClassDeclaration>().forEach { decl -> // decl.simpleName, decl.getAllFunctions(), nullability, etc. codeGenerator.createNewFile(/* ... */) } return emptyList() } } class MyProcessorProvider : SymbolProcessorProvider { override fun create(env: SymbolProcessorEnvironment): SymbolProcessor = MyProcessor(env.codeGenerator, env.logger) } ``` KSP walks **Kotlin symbols** directly (`KSClassDeclaration`, `KSFunctionDeclaration`, `KSPropertyDeclaration`) — **no Java stubs**. It understands nullability, `suspend`, extension functions, top-level functions, and properties natively. The result is typically **~2x faster** builds and is the **strategic direction** for Kotlin codegen. ## Applying KSP in Gradle ```kotlin plugins { kotlin("jvm") version "2.1.0" id("com.google.devtools.ksp") version "2.1.0-1.0.29" } dependencies { ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.0") } ``` Use the `ksp(...)` configuration (not `kapt(...)`). Many libraries — **Room, Moshi, Dagger/Hilt** — provide KSP processors. ## Decision - Prefer **KSP** whenever the library offers it. - Use **kapt** only for legacy processors that have no KSP equivalent. - Neither uses **runtime reflection** for the generated code — both emit sources at build time.

  • Can KSP and kapt coexist in one module?
    Yes, technically — you can apply both plugins and route some processors through kapt and others through KSP. But mixing reintroduces kapt's stub-generation cost, so migrate fully to KSP when possible.
  • Does KSP let you modify existing code?
    No. Like kapt, KSP can only generate new files; it cannot mutate existing declarations. To alter bytecode you would need a compiler plugin instead.

kapt is a translator who first rewrites your Kotlin into Java so an old reader can understand it; KSP is a reader fluent in Kotlin who skips the translation.

saying these in an interview costs you the question

  • Claiming annotation processing happens at runtime via reflection
  • Saying KSP runs standard javax.annotation.processing processors directly (it has its own API)
  • Not knowing kapt generates Java stubs and is the slow part
  • Thinking KSP can rewrite/modify existing code like a compiler plugin
  • Recommending kapt for greenfield code when a KSP processor exists

context