skip to content

You're designing the public API of a widely-used Kotlin library. How would you use @RequiresOptIn to manage API evolution and stability tiers, and what pitfalls would you guard against?

level: principalimportance: nice to knowfreq 18%

answer

  1. Small marker taxonomy = stability tiers
  2. Start WARNING, escalate to ERROR, then remove marker to graduate
  3. BINARY retention so enforcement crosses the jar boundary
  4. Don't @OptIn away leaky APIs or self-blanket your own markers
  5. Graduation is source-compatible for opt-in callers

basics

~20 s

Use opt-in markers to label which parts of your API are still experimental, so users must consciously accept the risk. Keep stable APIs unmarked, and have a clear plan for graduating experimental APIs to stable.

solid answer

~40 s

Define one or more marker annotations (@RequiresOptIn) representing stability tiers — e.g. an @ExperimentalApi for unstable surface and possibly an @InternalApi for things that are public for technical reasons but not meant for outside use. Tag unstable declarations with the marker; leave truly stable API unmarked. Drive severity with the level parameter: start new surface as WARNING, then escalate to ERROR before it matters. Document the marker's message clearly. Graduation = remove the marker once stable; that is binary-compatible for callers who used @OptIn, and removes their now-needless annotations over time. Pitfalls: don't @OptIn internally on leaky APIs (instability silently reaches callers); don't blanket -opt-in away your own markers (defeats the purpose); keep markers BINARY-retained so enforcement crosses module boundaries; and avoid marker proliferation — a small, well-documented set beats dozens.

code

kotlin · 10 lines
kotlin
@RequiresOptIn(
    "Experimental — may change in any minor release.",
    level = RequiresOptIn.Level.WARNING
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class ExperimentalMyLibApi

@ExperimentalMyLibApi
fun newShinyButUnstableFeature() { /* ... */ }

go deeper

for a junior

Understands markers label unstable APIs but not the lifecycle or library-design concerns.

for a middle

Can define markers and pick WARNING/ERROR, but may miss retention/leak and graduation nuances.

for a senior

Designs a coherent tier scheme, handles leaks via propagation, and knows graduation is source-compatible.

for a principal

Owns the stability policy: marker taxonomy, WARNING→ERROR→remove lifecycle, binary-compat reasoning, and team-wide conventions.

## Goal A popular library must ship new ideas without freezing them prematurely, yet protect users from depending on things that will change. `@RequiresOptIn` is the governance lever. ## Designing stability tiers Define a small, deliberate set of markers, each `@RequiresOptIn`-tagged: ```kotlin @RequiresOptIn( "Experimental API — may change in any minor release.", level = RequiresOptIn.Level.WARNING ) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) annotation class ExperimentalMyLibApi @RequiresOptIn( "Internal API — not for external use; no compatibility guarantees.", level = RequiresOptIn.Level.ERROR ) @Retention(AnnotationRetention.BINARY) annotation class InternalMyLibApi ``` - **Experimental tier**: real, intended-for-use, but unstable. Often `WARNING` initially. - **Internal tier**: public only because the language/module layout forces it; outsiders should never use it. Usually `ERROR`. - **Stable**: no marker at all. ## Lifecycle / graduation 1. Ship new surface marked `@ExperimentalMyLibApi` (WARNING). 2. Iterate on the design with real-world feedback. 3. When confident, **remove the marker** → the API becomes stable. Callers who wrote `@OptIn(ExperimentalMyLibApi::class)` keep compiling; their annotation is now a harmless no-op they can delete later. 4. Optionally escalate WARNING→ERROR mid-life if you want to discourage casual adoption before stabilizing. ## Retention matters Markers must be `@Retention(AnnotationRetention.BINARY)` so the compiler still enforces opt-in for *downstream* modules consuming your published artifact. `SOURCE` retention would lose enforcement across the jar boundary. ## Pitfalls to guard against - **Leaky `@OptIn` internally**: if your stable-looking function returns or accepts an experimental type, never just `@OptIn` it away — propagate, or wrap so nothing experimental leaks. Otherwise callers silently depend on unstable surface. - **Self-blanket `-opt-in`**: don't add your own experimental markers to your module's `optIn` list — you'd erase the very signal users rely on (and may stabilize-by-accident). - **Marker proliferation**: dozens of bespoke markers confuse users. Keep a small, documented taxonomy. - **Forgetting `@Target` constraints**: markers can't target EXPRESSION/FILE; design accordingly. - **No graduation plan**: experimental-forever erodes trust; tie markers to a versioning policy. ## Why opt-in beats alternatives Versus `@Deprecated` (for sunsetting) or just docs, opt-in gives a **compiler-enforced, greppable, per-use** acknowledgement — the strongest honest signal short of removing the API.

  • When you graduate an experimental API to stable by removing its marker, do existing callers break?
    No. Callers who wrote @OptIn for that marker still compile; the annotation just becomes a harmless no-op they can later delete. Removing a marker is source-compatible for them.
  • Why shouldn't you add your own experimental markers to your module's optIn build list?
    It would blanket-accept your own unstable surface internally, silencing the very signal that's supposed to keep YOU honest and possibly letting experimental APIs leak into stable ones unnoticed.

Opt-in markers are like a 'beta' wristband at an event: anyone wearing one knowingly accepted the rules, and when the feature graduates you simply stop handing out wristbands.

saying these in an interview costs you the question

  • Proposing dozens of bespoke markers with no documented policy
  • Self-opting-in to your own experimental markers via the build flag
  • Using SOURCE retention so downstream enforcement is lost
  • @OptIn-ing leaky internal usage instead of propagating or wrapping
  • No graduation/versioning plan — experimental forever

context