Your service's hot path uses kotlin-reflect to map data objects, and it's both slow and bloating the deployable. As a senior/principal, how do you decide whether to keep reflection, cache it, or eliminate the dependency entirely?
answer
- Measure before optimizing
- Rare → cache handles, keep JAR
- Hot/size-sensitive → codegen, drop kotlin-reflect
- kotlinx.serialization + KSP/kapt = zero runtime reflection
- GraalVM native image needs reflection config — codegen avoids it
basics
~20 sMeasure first. If reflection is only used a little, cache the resolved handles. If it's on a hot path or you're size-constrained, replace it with compile-time code generation so you can drop the library entirely.
solid answer
~40 sFrame the decision around three axes: call frequency, deployment-size sensitivity, and maintenance cost. First profile to confirm reflection — not something else — is the bottleneck. If reflection is incidental and rare, the cheapest fix is to resolve `KProperty`/`KFunction` handles once and cache them, removing per-call resolution overhead while keeping the dependency. If the path is hot or the target is size-sensitive (Android, serverless cold start, GraalVM native image), eliminate runtime reflection: use `kotlinx.serialization` (a compiler plugin, zero runtime reflection) for serialization, or KSP/kapt processors to generate direct mappers. Removing kotlin-reflect then also removes the JAR's size and any reflection-config burden on native images. The principal-level call also weighs team familiarity, build complexity of codegen, and whether a single shared mapping library should own this so the trade-off is made once, not per service.
code
kotlin · 9 linesimport kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
// Compiler-plugin serializer: NO kotlin-reflect at runtime,
// so the dependency can be dropped and the artifact shrinks.
@Serializable
data class Account(val id: Long, val owner: String, val active: Boolean)
fun encode(a: Account): String = Json.encodeToString(a)go deeper
Might say 'reflection is slow, avoid it' without a measured or nuanced plan.
Knows to cache handles and that kotlinx.serialization avoids runtime reflection.
Chooses between cache vs codegen based on frequency and size, and knows the native-image angle.
Makes it a measured, deployment-target-driven trade-off and centralizes the decision in a shared library/policy.
## Decision framework Don't reach for a fix before you've established what you're optimizing. Three questions drive the call: 1. **How often does the reflective path run?** (per request vs once at boot) 2. **How size/startup-sensitive is the deployable?** (Android APK, Lambda cold start, GraalVM native image vs a long-lived server) 3. **What's the maintenance/complexity budget?** (codegen adds build moving parts) ### Step 0 — measure Profile (JFR, async-profiler, or simple timing) to confirm kotlin-reflect is actually the cost. 'Reflection is slow' is a heuristic, not evidence; the hot spot may be elsewhere. ### Option A — keep reflection, cache the handles If reflection is incidental, the dominant cost is usually **re-resolving** members per call. Resolve once and reuse: ```kotlin import kotlin.reflect.full.memberProperties import kotlin.reflect.KProperty1 class Account(val id: Long, val owner: String, val active: Boolean) // Resolved a single time, reused for every mapping: private val accountProps: List<KProperty1<Account, *>> = Account::class.memberProperties.toList() fun map(a: Account): Map<String, Any?> = accountProps.associate { it.name to it.get(a) } ``` This keeps kotlin-reflect (so you still pay its size) but removes most per-call overhead. ### Option B — eliminate runtime reflection via codegen When the path is hot or size/startup matters, replace reflection with **compile-time** generation: - **kotlinx.serialization**: a compiler plugin emits serializers directly — no kotlin-reflect at runtime. - **KSP / kapt**: write or use an annotation processor that generates direct, monomorphic mapper functions. Direct generated code is JIT-friendly (inlinable, no boxing through generic reflective machinery) and lets you **drop kotlin-reflect** — shrinking the artifact and removing reflection-config headaches on **GraalVM native image** (reflective members otherwise need explicit registration). ### Native-image angle (often the deciding factor) GraalVM ahead-of-time compilation can't see reflective access statically; you must supply reflection configuration for every reflectively-touched member, or it fails at runtime. Codegen sidesteps this entirely, which is why size-and-AOT-constrained services tend to abandon runtime reflection. ## Putting it together - Rare + not size-critical → **cache handles**, keep the JAR. - Hot path **or** size/AOT-sensitive → **codegen**, drop kotlin-reflect. - Cross-cutting concern across many services → put the mapping in **one shared library** so the trade-off is decided and tuned once. The principal-level signal is making this an explicit, measured trade-off tied to the deployment target — not a reflexive 'reflection bad' or 'reflection is fine'.
- Why does dropping kotlin-reflect specifically help a GraalVM native image build?AOT compilation can't see reflective access; reflected members need explicit reflection config or they fail. Compile-time codegen produces direct calls the compiler can analyze, removing that config burden.
- If you must keep reflection, what's the single biggest per-call win?Resolve KProperty/KFunction handles once and cache them, instead of calling memberProperties/declaredFunctions on every invocation.
saying these in an interview costs you the question
- Optimizing before profiling the actual bottleneck
- Treating 'reflection bad' as an absolute with no trade-off
- Forgetting codegen lets you remove the JAR, not just speed it up
- Ignoring GraalVM reflection-config implications
- Re-resolving members per call when keeping reflection