skip to content

Why is KSP generally faster than kapt for annotation processing in a Kotlin project?

level: juniorimportance: must knowfreq 70%

answer

  1. kapt = Java stubs + javac
  2. KSP = Kotlin compiler plugin, no stubs
  3. no stub round-trip => ~2x faster
  4. KSP sees real Kotlin (nullability, defaults)
  5. processor must support KSP API

basics

~10 s

kapt first turns your Kotlin into fake Java code so old Java tools can read it, which is slow. KSP reads your Kotlin code directly, skipping that extra step, so the build is faster.

solid answer

~40 s

kapt (Kotlin Annotation Processing Tool) makes javac-based annotation processors work with Kotlin by generating Java 'stubs' — placeholder .java files representing your Kotlin code — then running the standard javax.annotation.processing pipeline on them. Generating stubs requires invoking the Kotlin compiler with extra work and round-tripping through the Java APT model, which is expensive. KSP (Kotlin Symbol Processing) is a Kotlin compiler plugin that exposes a Kotlin-native symbol API (KSClassDeclaration, KSFunctionDeclaration, Resolver). Processors read Kotlin symbols directly with no Java stub generation, so KSP commonly runs roughly 2x faster than kapt. KSP also understands Kotlin-specific constructs (nullability, default arguments, top-level functions) that the Java model can only approximate. The catch: a processor must be rewritten against the KSP API; kapt processors don't run on KSP unmodified.

go deeper

for a junior

Knows kapt is slower and KSP is the modern, faster replacement, even if fuzzy on stubs.

for a middle

Explains the Java stub round-trip as the cost and that KSP is a Kotlin compiler plugin reading symbols directly.

for a senior

Adds Kotlin-fidelity benefits (nullability, defaults) and notes the processor must be ported to the KSP API.

for a principal

Frames the ~2x as workload-dependent, discusses build-time impact at scale, and weighs ecosystem readiness when mandating KSP.

## What annotation processing is Annotation processing means a tool reads annotations (like `@Entity`, `@Inject`) in your source code at compile time and **generates extra source files** — for example Room generating DAO implementations, or Dagger generating dependency-injection wiring. This avoids hand-writing boilerplate. ## How kapt works (and why it's slow) `kapt` = **Kotlin Annotation Processing Tool**. Standard Java annotation processors use the `javax.annotation.processing` API and run inside **javac** (the Java compiler). javac cannot read Kotlin source. So kapt does a workaround: 1. The Kotlin compiler analyzes your Kotlin code and generates **Java stubs** — skeletal `.java` files that mirror your classes' signatures (no method bodies). 2. javac runs the annotation processors against those stubs using the normal Java APT model (`Element`, `TypeMirror`, `RoundEnvironment`). 3. Generated files are fed back into the Kotlin compilation. **Stub generation is the expensive part**: it forces an extra partial compilation pass and converts everything into a Java-shaped view of the world. ## How KSP works (and why it's faster) `KSP` = **Kotlin Symbol Processing**. It is a **Kotlin compiler plugin** with its own API that models Kotlin directly: - `Resolver` — entry point to query symbols. - `KSClassDeclaration`, `KSFunctionDeclaration`, `KSPropertyDeclaration` — Kotlin-native symbol types. - `SymbolProcessor` / `SymbolProcessorProvider` — what you implement. There is **no Java stub round-trip**. KSP reads Kotlin symbols straight from the compiler's resolved model, so it skips the whole stub-generation pass — the main reason it is commonly about **2x faster** than kapt. ## Bonus: better Kotlin fidelity Because KSP sees real Kotlin symbols, it understands constructs the Java stub model mangles or loses: **nullability** (`String?`), **default arguments**, **top-level/extension functions**, `suspend` functions, sealed classes, and more. ```kotlin // A KSP processor reads Kotlin directly: class MyProcessor(private val codeGen: CodeGenerator) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { val symbols = resolver.getSymbolsWithAnnotation("com.example.MyAnno") symbols.filterIsInstance<KSClassDeclaration>().forEach { decl -> // decl.simpleName, decl.getAllProperties(), nullability, etc. } return emptyList() } } ``` ## The trade-off KSP requires the **processor itself** to be written against the KSP API. A library shipping only a javac processor still needs kapt — see the migration-path question.

  • Does just enabling KSP make an existing kapt processor faster?
    No. Speed comes from the processor being implemented against the KSP API. A javac-only processor still needs kapt; you can't run it under KSP unchanged.
  • Name one Kotlin construct kapt's model handles poorly that KSP handles well.
    Nullability (e.g. String?). The Java stub view loses Kotlin's distinction between nullable and non-null types; KSP preserves it.

kapt is like translating a Kotlin book into rough Java first so a Java-only reader can skim it; KSP hands the reader a Kotlin edition directly.

saying these in an interview costs you the question

  • Claiming KSP is faster because it 'skips compilation entirely'
  • Saying any kapt processor automatically runs under KSP
  • Confusing KSP with simply turning off annotation processing
  • Not knowing kapt generates Java stubs
  • Thinking the speedup is about caching rather than no stub round-trip

context