skip to content

What is Kotlin's opt-in mechanism for experimental APIs (e.g. @OptIn / @RequiresOptIn), and how do you satisfy it at call sites and project-wide?

level: middleimportance: should knowfreq 40%

answer

  1. @RequiresOptIn marks unstable APIs (WARNING/ERROR)
  2. @OptIn(Marker::class) opts in locally
  3. Propagate by re-applying the marker
  4. Module-wide via compilerOptions.optIn / -opt-in flag
  5. Compile-time only, no runtime effect

basics

~20 s

Some Kotlin APIs are marked as experimental and may change. The compiler forces you to explicitly acknowledge that risk before using them — either by adding @OptIn at the call site or by enabling the opt-in for the whole module in the build file.

solid answer

~40 s

Library authors mark unstable APIs with an annotation that is itself annotated @RequiresOptIn (e.g. @ExperimentalCoroutinesApi). Using such an API without acknowledgement triggers a compiler error/warning. You opt in three ways: (1) propagate — annotate your own declaration with the marker so callers must also opt in; (2) local — annotate the using function/file with @OptIn(SomeMarker::class) to consume it without propagating; (3) module-wide — pass -opt-in=fully.qualified.Marker to the compiler, configured in Gradle via compilerOptions.optIn.add("kotlin.RequiresOptIn") or freeCompilerArgs. @RequiresOptIn carries a level of WARNING or ERROR. This is purely a compile-time gate: it documents API stability and prevents accidental dependence on volatile APIs; it has no runtime effect. Examples include @ExperimentalStdlibApi, @ExperimentalCoroutinesApi, and @DelicateCoroutinesApi.

code

kotlin · 14 lines
kotlin
// Library side
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalSearch

@ExperimentalSearch
fun fuzzySearch() { /* ... */ }

// Consumer side - local opt-in
@OptIn(ExperimentalSearch::class)
fun run() = fuzzySearch()

// Or module-wide in build.gradle.kts:
// kotlin { compilerOptions { optIn.add("com.example.ExperimentalSearch") } }

go deeper

for a junior

Knows some APIs are experimental and that @OptIn is how you acknowledge using them.

for a middle

Names @RequiresOptIn/@OptIn, the propagate vs local vs module-wide options, and that it is compile-time only.

for a senior

Explains WARNING vs ERROR levels, BINARY retention, and argues for narrow local opt-in over blanket module flags.

for a principal

Sets org-wide policy on which experimental markers are acceptable and governs module-wide opt-in to avoid silent dependence on volatile APIs.

## The problem it solves Kotlin and its libraries ship APIs at different stability levels. Some are **experimental** (signatures may change) or **delicate** (easy to misuse). The **opt-in mechanism** makes you *consciously acknowledge* that risk at compile time, so you never depend on a volatile API by accident. ## How an API is marked A library author defines a **marker annotation** and tags it with **`@RequiresOptIn`**: ```kotlin @RequiresOptIn( message = "This API is experimental and may change.", level = RequiresOptIn.Level.ERROR // or WARNING ) @Retention(AnnotationRetention.BINARY) annotation class MyExperimentalApi @MyExperimentalApi fun shinyButUnstable() { /* ... */ } ``` Real markers: `@ExperimentalStdlibApi`, `@ExperimentalCoroutinesApi`, `@DelicateCoroutinesApi`, `@ExperimentalUnsignedTypes`. ## The three ways to satisfy the requirement 1. **Propagate** — put the marker on *your* declaration. Now your function also requires opt-in, pushing the decision to your callers: ```kotlin @MyExperimentalApi fun myWrapper() = shinyButUnstable() ``` 2. **Opt in locally** — wrap the usage with **`@OptIn(Marker::class)`** on the function or file. You consume it; callers are unaffected: ```kotlin @OptIn(MyExperimentalApi::class) fun safeForCallers() = shinyButUnstable() ``` 3. **Module-wide** — tell the compiler to opt in everywhere via the **`-opt-in`** flag, configured in Gradle: ```kotlin kotlin { compilerOptions { optIn.add("kotlinx.coroutines.ExperimentalCoroutinesApi") } } // older style: freeCompilerArgs.add("-opt-in=...") ``` ## Severity `@RequiresOptIn(level = ...)` is either **WARNING** (compiles, warns) or **ERROR** (won't compile without opt-in). Authors pick based on how risky the API is. ## Important properties - It is a **compile-time only** gate — `@OptIn` and `@RequiresOptIn` have **no runtime behavior**; they only affect what the compiler accepts. - Retention is typically **BINARY**, so the requirement is enforced for consumers of the compiled library too. - Prefer **local `@OptIn`** over module-wide opt-in so the acknowledgement stays visible and narrow. ## Rule of thumb Use `@OptIn` narrowly at the call site; reserve module-wide `-opt-in` for markers you have deliberately decided to accept across the whole module.

  • What is the difference between @OptIn and propagating the marker?
    @OptIn consumes the experimental API and stops there — your callers don't need to opt in. Propagating (re-applying the marker on your own declaration) forces every caller to acknowledge it too.
  • Does opting in change anything at runtime?
    No. The opt-in mechanism is purely a compile-time stability gate; @OptIn/@RequiresOptIn have no effect on bytecode behavior at runtime.

It is like a 'sign here to acknowledge the risk' waiver before using a ride that might be retired tomorrow.

saying these in an interview costs you the question

  • Thinking @OptIn changes runtime behavior
  • Not knowing the WARNING vs ERROR level distinction
  • Always reaching for module-wide opt-in instead of narrow @OptIn
  • Confusing @OptIn with @Suppress (suppressing unrelated warnings)
  • Believing the requirement disappears once a library is compiled (BINARY retention enforces it for consumers)

context