Skip to main content

Overview

Viaduct uses explicit annotations to communicate the stability level of its APIs. This helps developers understand which parts of the framework are safe to depend on for production use and which are still evolving.
The Viaduct engine is in production at scale at Airbnb and has proven reliable. The developer API is under active development, and this system of annotations helps you understand which parts are stable and which may change.

Stability Annotations

Viaduct uses four primary annotations to mark API stability:

@StableApi

Long-term public contract for all consumers
  • Who should use it: All consumers, including production applications
  • Guarantees: Binary compatible within a major version; removal only via deprecation cycle
  • Behavior: Does not require opt-in; can be used freely
  • Example use cases: Core types, established resolver patterns, well-tested utilities
@StableApi
class PublicController {
    fun publicEndpoint() {} // Covered by class annotation
}

@ExperimentalApi

Public but evolving - use with caution
  • Who should use it: Early adopters willing to adapt to changes
  • Guarantees: May change or be removed in any release without deprecation
  • Behavior: Requires opt-in; produces compiler warning without @OptIn(ExperimentalApi::class)
  • Example use cases: New features, APIs under active development, proposals being validated
@ExperimentalApi
fun newCapability(): String = "v2"

// Usage requires opt-in
@OptIn(ExperimentalApi::class)
fun useIt() {
    newCapability()
}

@InternalApi

Internal implementation - not for consumers
  • Who should use it: Viaduct framework developers only
  • Guarantees: None - can change or disappear at any time
  • Behavior: Requires opt-in; produces compiler error without @OptIn(InternalApi::class)
  • Example use cases: Framework internals, implementation details, utilities for generated code
@InternalApi
class InternalOnlyService {
    fun methodA() {}
    fun methodB() {}
}

@VisibleForTest

Testing infrastructure only
  • Who should use it: Viaduct’s own test code
  • Guarantees: None - intended only for internal testing
  • Behavior: Requires opt-in; produces compiler error without opt-in
  • Example use cases: Test fixtures, diagnostics, internal testing utilities
@VisibleForTest
fun internalTestHook() { /* ... */ }

Stability Levels Summary

AnnotationMeaningWho Should UseGuaranteesOpt-in Required
@StableApiLong-term public contractAll consumersBinary compatible within major versionNo
@ExperimentalApiPublic but evolvingEarly adoptersMay change in any releaseYes (warning)
@InternalApiNot a consumer contractFramework developersNo stability guaranteeYes (error)
@VisibleForTestInternal testing onlyViaduct testsNo stability guaranteeYes (error)

Practical Guidance

For Application Developers

  1. Prefer @StableApi - Build your production applications on stable APIs whenever possible
  2. Use @ExperimentalApi selectively - Only opt-in to experimental APIs when:
    • You need cutting-edge features
    • You’re willing to adapt to changes
    • You isolate usage behind your own abstractions
  3. Avoid @InternalApi - These are framework internals and may break without warning
  4. Never use @VisibleForTest - These are for Viaduct’s internal tests only

Example: Mixing Stability Levels

@StableApi
class MyController {
    // Stable public method
    fun publicEndpoint() {}

    // Internal helper - not part of public API
    @InternalApi
    fun internalHelper() {}
}
In this example:
  • The class and publicEndpoint() are part of the stable public API
  • internalHelper() is an internal implementation detail

Deprecation and Migration

@Deprecated Annotation

When an API is scheduled for removal, it’s marked with @Deprecated instead of a stability annotation.
@Deprecated(
    message = "Use newFoo() instead.",
    replaceWith = ReplaceWith("newFoo()")
)
fun oldFoo() { /* ... */ }

@StableApi
fun newFoo() { /* ... */ }
Important notes:
  • Deprecated APIs do not carry stability annotations
  • When an API becomes deprecated, the previous stability annotation is removed
  • Migration paths are documented in the deprecation message
  • Removal typically happens only in major version updates

Upgrade Expectations

When upgrading Viaduct, expect different behaviors depending on the stability level:

Stable APIs (@StableApi)

  • Should remain binary compatible within a major version
  • Breaking changes coordinated via deprecation and major version bumps
  • Safe to depend on for production applications

Experimental APIs (@ExperimentalApi)

  • May change between releases, even minor versions
  • Re-test and re-validate call sites after upgrades
  • Consider isolating usage behind your own stable abstractions

Internal APIs (@InternalApi)

  • May break without warning in any release
  • If you opted in, you own the risk
  • Expect no migration path or deprecation cycle

Deprecated APIs

  • Follow migration guidance provided
  • Plan to remove usage before the next major version
  • Check release notes for removal timelines

Public API Surface

Viaduct’s external public surface is defined by:
  1. Kotlin visibility - What is technically public and visible outside the module
  2. Designated public packages - Packages intended for consumer use
Key modules with public APIs:
  • tenant/api - Tenant developer API (includes generated code)
  • service/api - Service configuration API
  • service/wiring - Service setup and wiring
Everything outside canonical public packages should be treated as implementation detail, even if it’s public in Kotlin.

Opt-in Configuration

Kotlin’s @RequiresOptIn mechanism enforces stability guarantees at compile time.

Module-level Opt-in

Internal Viaduct modules typically opt in to all stability levels:
kotlinOptions {
    freeCompilerArgs += listOf(
        "-opt-in=viaduct.api.InternalApi",
        "-opt-in=viaduct.api.ExperimentalApi",
        "-opt-in=viaduct.api.VisibleForTest"
    )
}

Call-site Opt-in

Consumer code should use @OptIn explicitly:
@OptIn(ExperimentalApi::class)
fun myExperimentalFeature() {
    // Use experimental APIs here
}

Binary Compatibility Validation

Viaduct uses the Kotlin Binary Compatibility Validator (BCV) to track and enforce the public binary API.
  • Declarations with @InternalApi or @VisibleForTest are excluded from .api dumps
  • Only stable and experimental APIs are tracked for binary compatibility
  • This ensures internal changes don’t affect the public API surface

For Contributors

If you’re contributing to Viaduct, see the API Stability Contributors Guide for detailed guidance on:
  • When to use each annotation
  • How annotations interact with BCV
  • Decision trees for choosing the right annotation
  • Examples of valid and invalid annotation usage

Questions?

If you have questions about API stability or which APIs to use:

Build docs developers (and LLMs) love