← Back to list

KAPT and KSP: why this migration exists at all

If you’ve worked on Android long enough, you’ve probably felt this without ever naming it:

Karishma Agrawal in Towards Dev · 2025-12-26 15:31 · 111 claps · 5.2 min read paywalled
#kapt #ksp #kotlin #kotlin-programming #android
Open on Medium ↗
Wiki topics: 💻 · Programming 📱 · Mobile Development 🔧 · Data Engineering

KAPT and KSP: why this migration exists at all

If you’ve worked on Android long enough, you’ve probably felt this without ever naming it:

“Why did my build just get slower after I added one annotation?”

That feeling is where KAPT enters the story.

The world KAPT came from

KAPT exists because Kotlin arrived late to a Java-first ecosystem.

When Android was Java-only, annotation processing was solved by JSR-269. Libraries like Dagger, Room, Glide, and AutoValue relied heavily on it to generate code at compile time. This was fast enough in Java, and everyone accepted it as the cost of doing business.

Then Kotlin became first-class on Android.

The problem? Java annotation processors cannot understand Kotlin code.

KAPT is Kotlin’s compromise.

Instead of asking Java processors to understand Kotlin, Kotlin says:

“Fine. I’ll pretend to be Java.”

So KAPT works by:

  1. Taking your Kotlin code
  2. Generating Java stubs that look “close enough”
  3. Running Java’s annotation processors on those stubs
  4. Feeding the generated code back into the build

It’s clever. It’s also expensive.

Every KAPT build means:

  • extra compilation phases
  • extra files
  • extra memory
  • extra Gradle work

And you feel that cost the moment your project stops being small.

Why KAPT feels slow (even when nothing is “wrong”)

KAPT isn’t slow because it’s badly written. It’s slow because it’s fundamentally a workaround.

The biggest issue is stub generation.

Kotlin has rich language features:

  • nullability
  • default parameters
  • extension functions
  • data classes
  • type aliases

Java processors don’t understand these. So Kotlin strips your code down into simplified Java-like stubs. That translation step alone costs time, and it happens every build.

On top of that:

  • incremental processing is harder
  • error messages are worse (often pointing to generated Java)
  • memory usage spikes
  • build scans show KAPT dominating compile time

For years, this was just… normal.

Why KSP had to exist

KSP exists because the Kotlin team finally said:

“We shouldn’t keep pretending Kotlin is Java.”

Instead of adapting Kotlin to Java processors, KSP flips the direction: Processors adapt to Kotlin.

KSP exposes Kotlin symbols directly:

  • classes
  • functions
  • properties
  • types
  • nullability
  • modifiers

No Java stubs. No pretending. No translation layer.

This one architectural change removes an entire phase from the build.

That’s why teams migrating to KSP often see:

  • 20–50% faster annotation processing
  • noticeably faster clean builds
  • much better incremental builds

Not because KSP is magic — but because it does less work.

Why migration is even a question

Here’s the important nuance people miss:

You don’t “choose” KAPT or KSP. Libraries do.

KAPT vs KSP is not a developer preference — it’s a library capability.

  • If a library ships a KAPT processor → you must use KAPT
  • If it ships a KSP processor → you can (and probably should) use KSP

That’s why migration is gradual and sometimes messy.

For a long time:

  • Dagger was KAPT-only
  • Room was KAPT-only
  • Moshi was KAPT-only

As these libraries started offering KSP support, migration became possible but not automatic.

Why teams migrate in practice

The real reasons teams migrate are rarely philosophical.

They’re practical:

  • “Our CI builds are too slow”
  • “KAPT takes 40% of our compile time”
  • “Incremental builds barely help anymore”
  • “Developers complain about rebuilds”

KSP migration usually starts as a build performance project, not a refactor.

And the moment you migrate one processor, you start asking:

“Why are we still paying the KAPT tax for everything else?”

The trade-offs no one likes to mention

KSP isn’t perfect.

Because it’s newer:

  • fewer processors support it
  • some edge cases behave differently
  • APIs are Kotlin-specific (writing processors requires Kotlin knowledge)

Also, you can run KAPT and KSP side by side, which means:

  • more plugins
  • more configuration
  • temporary complexity during migration

But importantly: KSP doesn’t replace annotation processing. It replaces how annotation processing happens.

The bigger picture

KAPT represents Kotlin adapting to the past. KSP represents Kotlin designing for its future.

As Android becomes more Kotlin-first:

  • new libraries ship KSP-only
  • old libraries deprecate KAPT support
  • build tooling optimizes around KSP

This isn’t just a performance upgrade. It’s the ecosystem slowly shedding a compatibility layer it no longer needs.

Now let’s talk about how the migration works

Migration Guide

Migrating from KAPT → KSP is really two changes:

  1. Replace the Gradle plugin that runs processors
  2. Replace each processor dependency ( kapt(...) ) with the library’s KSP artifact (the ksp(...) )

And there’s one rule that matters most:

You only get the full build speed win when you remove all kapt usage from a module.

Partial migration helps, but the real payoff comes when KAPT is gone entirely.

Step 0: inventory your current processors (don’t skip this)

Before touching Gradle, list what you currently have under:

  • kapt(...)
  • annotationProcessor(...)
  • (sometimes) kaptTest(...), kaptAndroidTest(...)

Typical ones:

  • Room compiler
  • Moshi codegen
  • Dagger/Hilt compiler
  • Glide compiler
  • AutoService, etc.

Because migration is only possible if the library has a KSP processor.

Step 1: apply the KSP Gradle plugin

Kotlin DSL (build.gradle.kts)

plugins {
  id("com.google.devtools.ksp") version "<ksp-version>"
}

Groovy (build.gradle)

plugins {
  id "com.google.devtools.ksp" version "<ksp-version>"
}

KSP versions are tied to Kotlin versions, so in real projects you usually align them via a version catalog or a single place in the root build. Kotlin’s quickstart covers the plugin setup.

Step 2: migrate processors one by one (the meat of it)

# Room

Old (KAPT):

kapt("androidx.room:room-compiler:<version>")

New (KSP):

ksp("androidx.room:room-compiler:<version>")

Room is one of the “common libs” explicitly listed as supporting KSP in Google’s migration doc.

# Moshi

Old:

kapt("com.squareup.moshi:moshi-kotlin-codegen:<version>")

New:

ksp("com.squareup.moshi:moshi-kotlin-codegen:<version>")

Moshi’s own repo states its Kotlin codegen can run as a KSP symbol processor.

# Glide

Glide does provide a KSP processor, but it does not support the deprecated Generated API.

If your project relies on GlideApp, GlideRequests, or similar generated types, you’ll need to remove those usages first.

Only projects that no longer depend on the generated API can migrate safely.

# Dagger / Hilt

This area evolves quickly, so don’t rely on old blog posts.

Dagger itself supports KSP, and AndroidX Hilt compiler support exists in newer versions. That said, many real-world projects still keep Hilt on KAPT while migrating other processors first.

One important detail: Dagger and Hilt often inspect types generated by other processors. If another processor generates types Dagger needs to see, that processor usually needs to be migrated as well — otherwise you can hit ordering and visibility issues.

This is why partial migration can feel flaky.

Step 3: remove kapt plugin only when you’re done

Once a module has no kapt(...) dependencies left, remove:

plugins {
  kotlin("kapt")
}

And delete any leftover config like:

kapt {
  correctErrorTypes = true
}

This is where the biggest build-time wins show up.

Step 4: fix the common breakages

1) Plugin not found errors Usually caused by mismatched Kotlin and KSP versions or incorrect plugin resolution.

2) Generated sources not being picked up

KSP generates into:

  • build/generated/ksp/...

This is normally wired automatically, but custom source sets or older tooling can interfere.

3) Mixing kapt + ksp in the same module

It works, but reduces the payoff and can introduce ordering confusion. Temporary mixing is fine — stopping halfway is not.

Conclusion

Migrating from KAPT to KSP isn’t about chasing the latest tool.

It’s about removing a compatibility layer your project no longer needs and getting faster, more predictable builds as a result.

And once you experience a KAPT-free module, it’s very hard to go back.

You can write back to me at karishma.agr1996@gmail.com if you’d like me to improve something in my upcoming articles. Your feedback is valuable.

Also, follow me on Medium and Linkedin

Your claps are appreciated to help others find this article 😃 .


메타데이터
post_id
c8d3d6a60e43
slug
kapt-and-ksp-why-this-migration-exists-at-all-c8d3d6a60e43
url
https://towardsdev.com/kapt-and-ksp-why-this-migration-exists-at-all-c8d3d6a60e43
canonical_url
https://towardsdev.com/kapt-and-ksp-why-this-migration-exists-at-all-c8d3d6a60e43
author_url
https://medium.com/@karishma-agr1996
status
ok
fetched_at
2026-06-15 20:49:13