What are the concrete costs of depending on kotlin-reflect — binary size, startup, and runtime — and how do you reason about them?
answer
- Three costs: size, first-use, per-call
- @Metadata parsed lazily on first reflection
- Cache KProperty/KFunction handles
- Codegen (KSP/kapt, kotlinx.serialization) avoids it
- Scope to testImplementation if only tests reflect
basics
~10 skotlin-reflect adds several megabytes to your app, makes startup a bit slower the first time you reflect, and each reflective lookup is much slower than a direct call. Use it sparingly and cache results.
solid answer
~40 skotlin-reflect carries three distinct costs. (1) Binary size: the artifact is a few megabytes of bytecode and metadata you ship — meaningful for Android APKs, fat JARs, or serverless cold-start images. (2) Startup/first-use cost: the first time you reflect over a class, kotlin-reflect parses the @Metadata annotation the compiler embeds and builds KClass/KProperty descriptor objects; this lazy initialization adds latency to first use, not to JVM boot. (3) Per-call runtime cost: reflective member access (.call, .get, memberProperties) is far slower than a direct method call and allocates. Mitigations: resolve reflection once and cache the KProperty/KFunction or a generated accessor; prefer ::class identity checks (cheap, stdlib-only) over full reflection; consider compile-time alternatives (KSP/kapt code generation, kotlinx.serialization's compiler plugin) that avoid runtime reflection entirely.
code
kotlin · 11 linesimport kotlin.reflect.full.memberProperties
import kotlin.reflect.KProperty1
class Row(val id: Long, val name: String)
// Resolve reflection ONCE at class-load, reuse forever:
private val rowProps: List<KProperty1<Row, *>> =
Row::class.memberProperties.toList()
fun toMap(r: Row): Map<String, Any?> =
rowProps.associate { it.name to it.get(r) }go deeper
Knows reflection is 'slower' and the library is 'big' without distinguishing the cost types.
Separates size, first-use, and per-call cost and knows to cache resolved handles.
Explains @Metadata-driven lazy initialization and chooses codegen vs reflection per workload.
Quantifies cost against deployment targets (APK, cold start) and sets org-wide policy on reflection use.
## Three separable costs When people say 'reflection is expensive' they conflate three different things. Separate them: ### 1. Binary size (ship cost) `kotlin-reflect` is a sizable JAR (multiple MB of compiled bytecode plus its own metadata). You pay this in: - **Android**: bigger APK/AAB, and it pulls in classes that affect method count and DEX/R8 work. - **Fat/uber JARs and container images**: a few MB that travels with every deploy. - **Serverless cold start**: more bytecode to load can lengthen cold starts. ### 2. Startup / first-use cost The Kotlin compiler stores per-class structural info in a `@Metadata` annotation on the generated `.class`. kotlin-reflect reads that metadata **lazily, on first reflective access** to a type, and materializes `KClass`, `KProperty`, `KFunction` descriptor objects. So the cost lands at *first reflection over a given class*, not at process boot. A program that never reflects pays nothing beyond the size cost; one that reflects over many classes at startup (DI containers, serializers warming up) sees a first-use hit. ### 3. Per-invocation runtime cost A reflective call is much slower than a direct one: - Lookups (`memberProperties`, `declaredFunctions`) traverse descriptors and allocate. - Invocation (`KFunction.call(...)`, `KProperty.get(...)`) goes through generic, boxing-prone machinery rather than a monomorphic call site the JIT can inline. ```kotlin import kotlin.reflect.full.memberProperties import kotlin.reflect.KProperty1 class Dto(val a: Int, val b: String) // BAD: re-resolves reflection on every call fun slow(d: Dto) = Dto::class.memberProperties.map { it.name to it.getter.call(d) } // BETTER: resolve once, reuse the KProperty handles private val props: List<KProperty1<Dto, *>> = Dto::class.memberProperties.toList() fun fast(d: Dto) = props.map { it.name to it.get(d) } ``` ## Mitigations (what a strong answer adds) - **Cache** resolved `KCallable`/`KProperty` handles; don't re-resolve per call. - **Prefer cheap paths**: `::class` identity/`isInstance` checks are stdlib-only and cheap; avoid full reflection when a `when`/`is` check suffices. - **Move work to compile time**: `kotlinx.serialization` (compiler plugin) and KSP/kapt code generation produce direct code with *no* runtime reflection — best for hot paths and size-sensitive targets. - **Scope the dependency**: if only tests reflect, keep kotlin-reflect on `testImplementation` so it never ships. ## Bottom line Reflection cost is dominated by repeated per-call overhead and by shipping the JAR; the metadata-parsing startup hit is one-time per class. Design so reflection happens once and is cached, or is eliminated via codegen.
- Does adding kotlin-reflect slow JVM startup even if you never reflect?Not the metadata-parsing part — that is lazy and per-class on first use. You still pay the size/class-loading cost of having a larger JAR on the classpath.
- Name a way to get reflection-like behavior with zero runtime reflection cost.Compile-time code generation: kotlinx.serialization's compiler plugin, or KSP/kapt processors that emit direct accessor code, so no kotlin-reflect is needed at runtime.
saying these in an interview costs you the question
- Claiming reflection cost is paid entirely at JVM boot
- Saying one reflective call is as fast as a direct call
- Re-resolving memberProperties inside a hot loop
- Not mentioning binary-size impact on Android/serverless
- Unaware that codegen can replace runtime reflection