skip to content

Explain the difference between SOURCE, BINARY, and RUNTIME retention, and which one you must use to read an annotation via reflection.

level: middleimportance: must knowfreq 50%

answer

  1. SOURCE → compiler only, then gone
  2. BINARY → in .class, invisible to reflection
  3. RUNTIME → in .class, visible to reflection
  4. Reflection needs RUNTIME
  5. Kotlin default = RUNTIME

basics

~10 s

SOURCE is dropped after compiling, BINARY stays in the class file but reflection can't see it, RUNTIME stays and reflection can read it. You need RUNTIME for reflection.

solid answer

~40 s

The three AnnotationRetention values form a visibility ladder. SOURCE means the compiler uses the annotation (lint, processors, suppression) then discards it — nothing is written to bytecode. BINARY means the annotation is emitted into the `.class` file, so other compilers/tools reading the bytecode metadata see it, but the JVM does not retain it for reflection, so `getAnnotations()` / `kClass.annotations` won't find it. RUNTIME means it is both written to bytecode and retained by the JVM so reflection (`kotlin.reflect.full.findAnnotation`, `KClass.annotations`, or Java's `AnnotatedElement.getAnnotation`) can read it. To inspect annotations at runtime — DI frameworks, serializers, validators — you must declare RUNTIME (which is also Kotlin's default).

code

kotlin · 12 lines
kotlin
@Retention(AnnotationRetention.SOURCE)
annotation class CompileOnly

@Retention(AnnotationRetention.RUNTIME)
annotation class ReadableAtRuntime

@CompileOnly @ReadableAtRuntime class Demo

fun main() {
    // Only ReadableAtRuntime shows up
    println(Demo::class.annotations.map { it.annotationClass.simpleName })
}

go deeper

for a junior

Can state the three levels survive progressively longer and reflection needs RUNTIME.

for a middle

Distinguishes BINARY (in bytecode, not reflective) from RUNTIME (in bytecode and reflective) and picks correctly per use case.

for a senior

Explains tooling/ABI implications of BINARY and runtime cost tradeoffs, names concrete reflection APIs.

for a principal

Reasons about retention as part of public-API contracts and tooling pipelines (KSP vs runtime frameworks).

## The retention ladder `@Retention(AnnotationRetention.X)` where X is one of three values, ordered by how far the annotation survives: ### SOURCE - Lives only in the source/compilation phase. - The compiler can act on it (suppress warnings, drive an annotation processor / KSP, emit lint) and then **discards** it. - **Not** present in the `.class` file, so neither bytecode tools nor reflection can see it. - Examples in the stdlib: `@Suppress`, `@DslMarker`-style source markers. ### BINARY - Written into the compiled `.class` file metadata. - Visible to tools that parse bytecode (other compilers, static analyzers, ABI checkers). - **Not** retained by the JVM at runtime, so reflection (`Class.getAnnotations()`, `KClass.annotations`) returns nothing for it. - Useful for cross-module compiler contracts that shouldn't pay the runtime cost. ### RUNTIME (default) - Written into the `.class` file **and** kept by the JVM so it can be read reflectively. - This is the only level readable with reflection. ## Reading reflectively ```kotlin import kotlin.reflect.full.findAnnotation @Retention(AnnotationRetention.RUNTIME) annotation class Marker @Marker class A @Retention(AnnotationRetention.SOURCE) annotation class GoneAtRuntime @GoneAtRuntime class B fun main() { println(A::class.findAnnotation<Marker>()) // Marker@... println(B::class.annotations) // [] — SOURCE is gone } ``` `findAnnotation<T>()`, `KClass.annotations`, `KFunction.annotations`, and Java's `AnnotatedElement.getAnnotation` all require **RUNTIME** retention to return anything. ## How to choose - Need a DI/serialization/validation framework or your own reflective code to see it at runtime? **RUNTIME**. - A compile-time-only marker (KSP/processor, lint, suppression)? **SOURCE** — avoids bloating bytecode. - A bytecode-level contract for other compilers but not runtime? **BINARY** (rare in app code).

  • Why might you choose BINARY over RUNTIME for a compiler-level annotation?
    BINARY keeps it in bytecode for tools/other compilers without paying the JVM's runtime-retention cost or exposing it to reflection.
  • Will a SOURCE-retained annotation ever appear in KClass.annotations?
    No — it is discarded after compilation, so reflection returns an empty list for it.

SOURCE is a sticky note you toss after reading; BINARY is ink that dries inside a sealed box; RUNTIME is ink on the outside you can read anytime.

saying these in an interview costs you the question

  • Saying BINARY annotations are readable via reflection
  • Believing reflection can recover SOURCE annotations
  • Not knowing RUNTIME is required (and default) for reflective reads
  • Confusing retention with use-site targets

context