The Real Cost of Kotlin Reflection

AndroidKotlin
Share on LinkedIn

A startup trace showed 180ms in kotlin.reflect.jvm.internal during first frame—before any user interaction. The culprit was a DI-adjacent library walking KClass members at init to find @Serializable types. Replacing that scan with KSP-generated registries dropped cold start by 120ms and removed 1.4MB DEX. Reflection is convenient until you measure it on a mid-range phone.

Kotlin reflection (kotlin-reflect) exposes types, functions, and properties at runtime. On server JVMs the cost is often ignored. On Android and Native, metadata parsing, allocation, and ProGuard complexity make reflection a deliberate choice—not a default.

What reflection costs

Micro-benchmarks on member lookup:

// Avoid in hot paths
fun inspect(obj: Any) {
    obj::class.members.filter { it.name.startsWith("get") }
}

Each call walks metadata. Fine in a debugger tool; wrong in RecyclerView binding.

Reified generics: zero-cost at call site

inline fun <reified T> Fragment.findNavController(): NavController {
    // T available at compile time in inline body
}

inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(): T? {
    return if (Build.VERSION.SDK_INT >= 33) {
        getParcelableExtra(key, T::class.java)
    } else {
        @Suppress("DEPRECATION")
        getParcelableExtra(key) as? T
    }
}

reified inlines type info—no T::class field unless you explicitly reference it.

kotlinx.serialization instead of Gson reflection

Gson reflects fields at runtime. kotlinx.serialization generates codecs:

@Serializable
sealed interface PaymentResult {
    @Serializable data class Success(val id: String) : PaymentResult
    @Serializable data class Failure(val code: Int) : PaymentResult
}

val json = Json { classDiscriminator = "type" }
json.decodeFromString<PaymentResult>(payload)

Polymorphic without classpath scanning when registered explicitly.

KSP for registries

Generate type maps at compile time:

// Generated by KSP
object ScreenRegistry {
    val routes: Map<String, () -> Screen> = mapOf(
        "home" to { HomeScreen() },
        "profile" to { ProfileScreen() }
    )
}

Navigation and plugin systems use this pattern to drop Class.forName.

R8 and keep rules

If reflection is unavoidable:

-keep class com.example.model.** { *; }
-keepattributes RuntimeVisibleAnnotations

Missing rules cause production-only crashes. Document every reflective access.

Prefer @Keep on specific classes over broad wildcards.

JVM/server: when reflection is fine

Admin tools, test fixtures, framework internals (Spring, ktor routing with reified routes) run reflection at startup once. Profile before optimizing.

Avoid reflection in:

Migration checklist

  1. Audit kotlin-reflect dependency—exclude transitive pulls
  2. Replace Gson/Moshi reflective adapters with codegen (KSP)
  3. Replace manual KClass walks with generated catalogs
  4. Use inline reified at API boundaries
  5. Measure release build startup with Macrobenchmark

Benchmark methodology

Compare cold start with Android Macrobenchmark, not microbenchmarks on desktop JVM. Reflection cost hits mobile cold start disproportionately.

Moshi codegen vs KSP

Moshi Kotlin codegen via KSP eliminates reflective adapter lookup at runtime—pair with ProGuard keep rules only for models, not entire reflect library.

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.

Common questions from reviewers

Reviewers and auditors often ask whether this approach scales with team growth and whether it fails safely. Answer explicitly in your design doc: what happens when dependencies are down, when credentials expire, and when traffic doubles overnight. Prefer defaults that deny or degrade gracefully over defaults that fail open. Document known limits (throughput ceilings, supported versions, regions) in the same place operators look during incidents—avoid scattering critical constraints across Slack threads.

Resources

Frequently asked questions

Why is kotlin-reflect large on Android?

The kotlin-reflect JAR embeds metadata parsers and caches for the entire Kotlin type system. It adds megabytes to APK size and initialization cost. R8 can shrink unused portions but reflection-heavy libraries still pull significant DEX weight.

When is reified type parameter acceptable instead of KClass?

Inline functions with reified T avoid runtime KClass lookups for generic type parameters at call sites. Use reified for kotlinx.serialization serializers(), Retrofit-style parsers, and internal DSLs—not for storing types in collections across non-inline boundaries.

What replaces runtime reflection for JSON polymorphism?

kotlinx.serialization with sealed class serializers generated at compile time, or explicit JsonContentPolymorphicSerializer. KSP processors can generate type maps for custom polymorphic registries without scanning classpath at runtime.

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 →