How do AnnotationRetention values (SOURCE, BINARY, RUNTIME) interact with reading meta-configured annotations, and what is Kotlin's default?
answer
- SOURCE -> compile only; BINARY -> class file, no reflection; RUNTIME -> reflectable
- Kotlin default = RUNTIME (Java default = CLASS)
- RUNTIME required for findAnnotation/findAnnotations
- SOURCE invisible to bytecode and reflection
- Tighten retention to the actual use case
basics
~10 s@Retention sets how long an annotation survives: SOURCE (compile-time only), BINARY (in the class file), or RUNTIME (readable by reflection). Kotlin defaults to RUNTIME.
solid answer
~40 s`@Retention(value)` takes an `AnnotationRetention`: `SOURCE` keeps the annotation only during compilation — it is discarded and invisible to both bytecode and reflection (good for compiler/IDE hints like lint markers); `BINARY` stores it in the `.class`/`.kotlin_metadata` but the JVM does not expose it to reflection; `RUNTIME` stores it and makes it readable via reflection (`KClass.annotations`, `findAnnotation`, `findAnnotations`). **Kotlin's default is `RUNTIME`** — a deliberate contrast with Java, whose default is `CLASS` (binary). This matters when designing annotations meant to be read at runtime (DI, serialization, validation): you must keep the default or explicitly set `RUNTIME`, otherwise reflection sees nothing. SOURCE-retained annotations also can't be processed by reflection-based frameworks, only by annotation processors / the compiler.
code
kotlin · 8 lines@Retention(AnnotationRetention.RUNTIME)
annotation class Endpoint(val path: String)
@Endpoint("/users")
class UsersController
import kotlin.reflect.full.findAnnotation
val p = UsersController::class.findAnnotation<Endpoint>()?.path // "/users"go deeper
Lists the three retention levels and that RUNTIME is needed for reflection.
Knows Kotlin's default is RUNTIME and can show findAnnotation working only at RUNTIME.
Articulates the Java-vs-Kotlin default trap, the BINARY-vs-RUNTIME distinction, and chooses retention per use case.
Reasons about metadata footprint, build-time (KSP/kapt) vs runtime processing trade-offs, and interop consequences of retention choices across a public library.
## The three retention levels `@Retention` is meta-annotated onto your annotation class and takes one `AnnotationRetention` value: - **`SOURCE`** — kept only while the compiler runs, then discarded. It is **not** written to the `.class` file and is **not** visible to reflection. Use it for source-level hints consumed by the compiler, lint, or IDE (e.g. suppression-style markers). - **`BINARY`** — written into the compiled artifact (bytecode / Kotlin metadata) but **not** exposed by JVM reflection. Useful when an external bytecode tool or annotation processor reads it, but runtime reflection should not. - **`RUNTIME`** — written into the artifact **and** exposed to reflection. This is what frameworks (DI, serialization, validation, web routing) need. ## Kotlin's default differs from Java ```kotlin annotation class Marker // retention defaults to RUNTIME in Kotlin ``` **Kotlin defaults to `RUNTIME`.** Java's `@Retention` defaults to `RetentionPolicy.CLASS` (binary). This is a classic interview trap: if you mentally apply Java's default, you'll wrongly assume a bare Kotlin annotation is invisible to reflection. ## How retention gates reflective reading ```kotlin import kotlin.reflect.full.findAnnotation @Retention(AnnotationRetention.RUNTIME) annotation class Endpoint(val path: String) @Endpoint("/users") class UsersController val ann = UsersController::class.findAnnotation<Endpoint>() println(ann?.path) // "/users" — works because retention is RUNTIME ``` If `Endpoint` were `@Retention(SOURCE)` or `BINARY`, `findAnnotation` would return `null` — the annotation simply isn't there to read. ## Interaction with other meta-annotations - **`@Repeatable`**: to read *all* repeated occurrences via `findAnnotations<T>()`, the annotation must be `RUNTIME`. - **`@Target`**: orthogonal — controls placement, not lifetime. - **`@MustBeDocumented`**: orthogonal — controls docs, not lifetime. ## Design guidance - Reflection-driven framework annotation -> `RUNTIME` (or rely on the default). - Annotation processed only at build time by KSP/kapt -> `BINARY` or `SOURCE` to avoid leaking into runtime metadata. - Pure compiler/IDE hint -> `SOURCE`. Keeping retention as tight as the use case allows reduces metadata footprint and avoids exposing internal markers to reflection.
- A teammate ports a Java annotation to Kotlin and removes @Retention; reflection now sees it unexpectedly. Why?Java defaulted to CLASS (binary, no reflection); Kotlin defaults to RUNTIME. Dropping the explicit retention flipped it to reflectable. They should re-add @Retention(BINARY) if runtime visibility wasn't intended.
- When would you deliberately choose SOURCE retention?For annotations consumed only by the compiler, IDE, or KSP at build time — lint/suppression markers, code-gen triggers — where there's no value in carrying them into bytecode or exposing them to reflection.
Like ink that fades at different times: SOURCE vanishes after compiling, BINARY stays on the page but is invisible to the runtime reader, RUNTIME stays fully readable.
saying these in an interview costs you the question
- Saying Kotlin's default retention is BINARY/CLASS
- Claiming SOURCE-retained annotations are readable by reflection
- Confusing BINARY (in class file, not reflectable) with RUNTIME
- Thinking @Target affects retention
- Assuming reflection works regardless of retention