You're authoring a Kotlin library where the default `public` visibility risks leaking implementation details. What language/tooling mechanisms help control the public API surface?
answer
- explicitApi() / -Xexplicit-api=strict
- Forces explicit modifier + return type on API
- Default-deny: mark internals internal/private
- binary-compatibility-validator: apiDump/apiCheck
- @PublishedApi internal for inline functions
basics
~20 sTurn on Kotlin's explicit-API mode so the compiler forces you to label every public declaration on purpose. Mark internals with internal or private, and use tools that track your public API so accidental changes are caught.
solid answer
~40 sBecause Kotlin's default is `public`, library authors enable **explicit API mode** (`explicitApi()` / `-Xexplicit-api={strict|warning}` in the Kotlin Gradle DSL). In strict mode the compiler **requires an explicit visibility modifier** on every API declaration and an explicit return type on public functions/properties, so nothing is exposed by accident. Implementation details are then deliberately marked `internal` (module-scoped) or `private` (file/class-scoped). To prevent silent API drift, teams add **binary-compatibility tracking** (the Kotlin `binary-compatibility-validator` / `apiDump`/`apiCheck` tasks) which snapshots the public ABI and fails the build on unintended changes. `ktlint`/`detekt` can additionally enforce conventions. Together these turn a public-by-default language into a deliberate, reviewable API surface.
code
kotlin · 11 lines// build.gradle.kts
kotlin {
explicitApi() // strict: every API needs an explicit modifier + return type
}
// Inline function needing an internal helper:
public inline fun <T> measure(block: () -> T): T {
return timed(block) // timed is internal but reachable via @PublishedApi
}
@PublishedApi
internal fun <T> timed(block: () -> T): T = block()go deeper
Knows you mark internals private/internal to hide them.
Adds that the public default is risky for libraries and that internal scopes to the module.
Names explicit API mode and what strict mode enforces; uses @PublishedApi internal for inline functions.
Designs end-to-end API governance: explicit-API strict + default-deny visibility + binary-compatibility-validator in CI, reasoning about ABI stability and review ergonomics.
## The problem Kotlin's default visibility is **`public`**. In an application that's fine, but in a **published library** every unmarked top-level or member declaration becomes part of the API you must keep compatible. Implementation details leak easily. ## Explicit API mode Kotlin offers **explicit API mode**, enabled per-module in the Gradle Kotlin DSL: ```kotlin kotlin { explicitApi() // == strict // or: explicitApiWarning() } // equivalently: -Xexplicit-api=strict | warning ``` In **strict** mode the compiler **errors** when: - a declaration that is effectively part of the public/published API has **no explicit visibility modifier** (you must write `public`/`internal`/`private` deliberately), and - a public function or property **omits its return type** (preventing accidental type changes). In **warning** mode the same conditions produce warnings instead of errors. This flips the ergonomics: instead of 'public unless hidden,' you get 'you must *choose* visibility,' surfacing every exposure decision in review. ## Marking internals With explicit API on, you then deliberately apply: - **`internal`** — share across the module's files but hide from consumers (the bulk of implementation helpers). - **`private`** — file/class-local details. - **`public`** — only the genuine API. ## Tracking the binary API Visibility alone doesn't stop *intentional public* declarations from changing incompatibly. The **Kotlin binary-compatibility-validator** Gradle plugin maintains an `.api` dump of the public ABI: - `apiDump` regenerates the snapshot; - `apiCheck` fails the build if the current public surface diverges from the committed snapshot. This makes any addition/removal/signature change to the public API a **visible, reviewed diff**, catching accidental leaks (e.g. forgetting `internal`). ## Supporting tooling - **`detekt`/`ktlint`** enforce style and can flag missing modifiers / discourage broad visibility. - IDE inspections surface 'declaration could be private/internal.' - `@PublishedApi internal` lets an `internal` symbol be referenced from a `public inline` function's body without becoming public API in the normal sense — relevant when inline functions must call into internals. ## Putting it together (library hygiene) 1. Enable `explicitApi()` strict. 2. Default-deny: mark helpers `internal`/`private`; only the deliberate surface is `public`. 3. Commit an API dump and run `apiCheck` in CI. 4. Use `@PublishedApi internal` for inline-function internals. The result: the public-by-default language is converted into an **opt-in, reviewable, compatibility-checked** API surface. ```kotlin // strict explicit API public fun parse(input: String): Result = Parser(input).run() // explicit modifier + return type internal class Parser(private val raw: String) { // hidden impl fun run(): Result = TODO() } ```
- What two things does strict explicit API mode require that normal mode doesn't?An explicit visibility modifier on every API declaration, and an explicit return type on public functions/properties — so nothing is exposed or typed by accident.
- Why use `@PublishedApi internal` instead of just `internal`?A `public inline` function's body is copied into callers in other modules, so it can't reference a plain `internal` symbol. `@PublishedApi internal` permits that reference while keeping the symbol out of the normal public surface.
- How do you stop an intended public API from changing incompatibly?Use the binary-compatibility-validator: commit an `.api` dump and run `apiCheck` in CI so any public ABI change is a reviewed diff; `apiDump` regenerates the snapshot intentionally.
Like requiring every door in a building to be labeled 'public' or 'staff' before opening day, plus a logbook that flags any door that silently changes.
saying these in an interview costs you the question
- Relying only on the public default and hoping reviewers catch leaks
- Not knowing explicit API mode exists
- Confusing visibility control with binary-compatibility checking (they're complementary)
- Using plain `internal` inside a public inline function (won't compile across modules)
- Treating `internal` as sufficient API governance without a compatibility check