skip to content

How do AnnotationRetention values (SOURCE, BINARY, RUNTIME) interact with reading meta-configured annotations, and what is Kotlin's default?

level: seniorimportance: must knowfreq 40%

answer

  1. SOURCE -> compile only; BINARY -> class file, no reflection; RUNTIME -> reflectable
  2. Kotlin default = RUNTIME (Java default = CLASS)
  3. RUNTIME required for findAnnotation/findAnnotations
  4. SOURCE invisible to bytecode and reflection
  5. 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
kotlin
@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

for a junior

Lists the three retention levels and that RUNTIME is needed for reflection.

for a middle

Knows Kotlin's default is RUNTIME and can show findAnnotation working only at RUNTIME.

for a senior

Articulates the Java-vs-Kotlin default trap, the BINARY-vs-RUNTIME distinction, and chooses retention per use case.

for a principal

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

context