KAPT vs KSP: Why You Should Migrate

AndroidKotlin
Share on LinkedIn

Our Android monorepo's :feature:checkout module spent 47 seconds in kaptKotlin on every clean build—mostly regenerating Room DAOs and Dagger factories from Java stubs that existed only because KAPT couldn't read Kotlin types natively. Switching that module to KSP cut annotation processing to 19 seconds and eliminated an entire stub-generation pass. If you're still on KAPT in 2025, you're paying a tax on every CI run for historical reasons, not technical ones.

KAPT (Kotlin Annotation Processing Tool) compiles Kotlin to Java stubs, then feeds them to Java annotation processors. It works, but it's indirect. KSP (Kotlin Symbol Processing) reads Kotlin AST directly, generates Kotlin-friendly code, and integrates with Kotlin's incremental compilation.

How the pipelines differ

KAPT path:

Kotlin sources → stub .java files → javac + processors → generated code → Kotlin compile

KSP path:

Kotlin sources → KSP processors → generated Kotlin/Java → compile

No stub layer means fewer files, less invalidation, and processors see actual nullability, default parameters, and visibility.

Enable KSP:

// build.gradle.kts
plugins {
    id("com.google.devtools.ksp") version "2.0.21-1.0.28"
}

dependencies {
    ksp("androidx.room:room-compiler:2.6.1")
    // NOT kapt(...)
}

Remove the matching kapt dependency when the processor supports KSP exclusively.

Migrating Room

Room first-class supports KSP:

plugins {
    id("com.google.devtools.ksp")
}

android {
    defaultConfig {
        ksp {
            arg("room.schemaLocation", "$projectDir/schemas")
            arg("room.incremental", "true")
        }
    }
}

dependencies {
    implementation("androidx.room:room-runtime:2.6.1")
    ksp("androidx.room:room-compiler:2.6.1")
}

Generated sources appear in build/generated/ksp/<variant>/kotlin. Update .gitignore if you ignored kapt paths but commit nothing from build—schema export directory stays the same.

Verify DAO queries compile—KSP catches more Kotlin-specific errors at processing time than KAPT stubs sometimes masked.

Migrating Dagger and Hilt

Hilt supports KSP starting with recent AGP/Kotlin pairings:

plugins {
    id("com.google.devtools.ksp")
    id("com.google.dagger.hilt.android")
}

dependencies {
    ksp("com.google.dagger:hilt-compiler:2.52")
    ksp("androidx.hilt:hilt-compiler:1.2.0") // for @HiltViewModel etc.
}

Drop kotlin-kapt plugin when no kapt dependencies remain. Mixed modules during migration can use both in different subprojects—avoid both in the same module unless necessary.

Custom annotation processors

If you maintain in-house processors, KSP's API differs from javax.lang.model:

class MyProcessor(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger
) : SymbolProcessor {
    override fun process(resolver: Resolver): List<KSAnnotated> {
        val symbols = resolver.getSymbolsWithAnnotation("com.example.Bind")
        symbols.filterIsInstance<KSClassDeclaration>().forEach { /* generate */ }
        return emptyList()
    }
}

KSP cannot reuse Java Annotation Processing API code verbatim. Budget rewrite time if you own processors.

When KAPT still hangs around

Run ./gradlew :app:kspDebugKotlin --profile vs kapt equivalent to measure locally before mass migration.

Migration checklist

  1. Upgrade Kotlin, AGP, and processor libraries to KSP-compatible versions
  2. Add KSP plugin at root with aligned version string
  3. Replace kapt with ksp per dependency
  4. Move processor args from kapt { arguments { } } to ksp { arg() }
  5. Remove kotlin-kapt plugin module-by-module
  6. Clean build and fix any generated import paths in tests

CI cache keys should include KSP outputs—stale kapt caches cause confusing errors after switching.

CI cache keys

Include Kotlin version, KSP version, and processor classpath in Gradle cache key—stale KSP output after processor upgrade causes bizarre compile errors in generated sources.

Incremental builds

KSP symbol-processing-api incremental mode requires processors to declare dependencies correctly—if builds are always clean, check processor implements Incremental contract.

What to measure after rollout

Track error rates, tail latency, and resource utilization for two weeks after changes land—most regressions appear under real traffic mixes, not in staging smoke tests. Keep a rollback path documented: feature flags, Helm revision, or Git revert with known good digest. Review on-call pages tied to the topic quarterly; delete alerts that never fire and add thresholds that would have caught your last incident.

Run a short blameless postmortem if production surprised you, even for minor issues. The goal is updating this runbook section with one concrete lesson per quarter so the next engineer inherits context, not just configuration snippets.

Documentation your team should maintain

Maintain a one-page runbook link from your main service README: prerequisites, owner rotation, last drill date, and known sharp edges. Link to vendor docs in the Resources section below but capture org-specific decisions (CIDR ranges, cluster names, approval gates) in internal docs that stay current. New hires should deploy a safe canary within a week using only that runbook—if they cannot, the doc is incomplete.

Pre-production checklist

Before promoting to production, walk through this list with someone who was not the primary author—fresh eyes catch assumptions.

If any item is "we will do that later," treat it as a release blocker for tier-1 services.

Resources

Frequently asked questions

Is KSP a drop-in replacement for KAPT?

Not always. KSP understands Kotlin directly and exposes a different API for processor authors. Libraries must ship KSP-compatible processors—Room, Moshi, and Hilt support KSP, but older or Java-only processors may still require KAPT. You can run KSP and KAPT together during migration, but the goal is to drop KAPT entirely.

How much faster are KSP builds compared to KAPT?

Teams typically report 1.5x to 2x faster annotation processing modules, with larger projects seeing more because KAPT generates stub Java sources and runs javac. Exact gains depend on module count and processor workload. The bigger win is incremental correctness—KSP avoids stub-related full recompiles.

What breaks most often during KAPT to KSP migration?

Processors that relied on Java-only type information or Element APIs need KSP rewrites if you maintain custom processors. For consumers, misconfigured processor arguments and missing ksp() dependencies are common. Room needs the ksp plugin and schema export path updated; generated code lands under build/generated/ksp instead of kapt.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →