skip to content

You lead a multi-module Kotlin library. Design an adoption strategy for Explicit API mode that doesn't paralyze the team, and explain the trade-offs.

level: seniorimportance: nice to knowfreq 14%

answer

  1. Warning -> triage -> fix in PRs -> strict -> CI
  2. Convention plugin so all modules inherit the policy
  3. Main source sets only; skip tests/samples/apps
  4. Pair with BCV + deprecation policy + SemVer
  5. Front-loaded cost, compounding benefit

basics

~20 s

Turn it on as warnings first so nothing breaks, clean up module by module, then switch to strict and let CI enforce it. Apply it only to published modules, not apps or tests, and standardize the setting in one shared build convention.

solid answer

~40 s

Roll it out incrementally. Start by enabling explicitApiWarning() so the compiler surfaces every offending declaration without failing builds; this turns a wall of errors into a backlog. Triage per module — public-facing API modules first, internal/impl modules later or never. Fix declarations in focused PRs, then flip each finished module to explicitApi() (strict) and wire it into CI. Centralize the setting in a Gradle convention/precompiled-script plugin so every library module inherits the same policy rather than copy-pasting. Scope it to main source sets, not tests or samples. Pair it with the Binary Compatibility Validator so you also catch surface drift, and a deprecation policy for removals. Trade-offs: upfront churn and noisier signatures versus a curated, review-friendly, stable public API; the cost is front-loaded and pays off as the library and its consumer base grow.

go deeper

for a junior

Suggests just turning it on; may not foresee the migration pain.

for a middle

Proposes warning-then-strict and scoping to library modules.

for a senior

Adds per-module triage, a convention plugin, CI enforcement, and pairing with BCV/deprecation.

for a principal

Owns the trade-off curve — decides which modules are worth strict mode, sequences it with release trains, and folds it into broader API governance and SemVer policy.

## Goal Get to **strict** Explicit API mode on the published surface without a big-bang failure that blocks everyone. ## Phased rollout 1. **Warning first.** Set `explicitApiWarning()` (or `-Xexplicit-api=warning`) repo-wide. Builds keep passing; the compiler now **lists** every public/protected declaration missing visibility or a return type. This converts an overwhelming error wall into a measurable backlog. 2. **Triage by module.** Prioritize **consumer-facing API modules**; defer or skip `internal`-heavy implementation modules where the curation buys little. Decide which modules will ever be strict. 3. **Fix in focused PRs.** Add `public`/`internal`/`private` keywords and explicit return types. Frequently the fix is to **downgrade** something to `internal` that was never meant to be public — a real bug caught. 4. **Flip to strict per module.** Once a module is clean, switch it to `explicitApi()` and add it to the **CI gate** so regressions fail fast. 5. **Centralize the policy.** Encode the setting in a **convention plugin** (precompiled `*.gradle.kts` in `buildSrc`/an included build) so every library module opts in identically: ```kotlin // buildSrc: mylib.library-conventions.gradle.kts kotlin { explicitApi() } ``` Modules then just `plugins { id("mylib.library-conventions") }`. ## Scoping - **Main/library source sets only.** Tests and samples don't need a curated surface — leaving them off avoids pointless churn. - **Apps don't need it at all** — it's a library concern. ## Complementary controls - **Binary Compatibility Validator** for surface drift (`apiCheck`/`apiDump`). - A **deprecation policy** (`@Deprecated`, `DeprecationLevel.WARNING -> ERROR -> HIDDEN`) and **SemVer** for the actual remove/change decisions. ## Trade-offs | Benefit | Cost | |---------|------| | Public surface is intentional & reviewable | Upfront churn fixing many declarations | | Catches accidental exposure (often real bugs) | Signatures are more verbose | | Stable signatures (no surprise inferred-type changes) | Slight friction adding new public API | | Pairs cleanly with BCV in CI | Low payoff for tiny/internal-only modules | The cost is **front-loaded**; the benefit **compounds** as the API and its consumer base grow, which is exactly why it's a library (not app) tool. The principal-level judgment is *where the line is*: not every module deserves strict mode.

  • Why not just turn on strict mode everywhere immediately?
    On a large codebase it would produce a flood of build-breaking errors at once, blocking all work; warnings let you burn the backlog down incrementally.
  • How do you keep the policy consistent across 20 modules?
    Put explicitApi() in a shared Gradle convention plugin that each library module applies, instead of duplicating the setting.
  • Which modules might you deliberately leave un-strict?
    Internal/implementation-only modules and test/sample source sets, where a curated public surface adds little value.

Like fixing tech debt: don't fail the build on day one — light it up as warnings, burn down the backlog, then lock the door.

saying these in an interview costs you the question

  • Proposing big-bang strict mode on the whole repo with no migration
  • Applying it to test and application modules indiscriminately
  • Copy-pasting the setting into every module instead of a convention plugin
  • Treating it as a substitute for BCV or SemVer
  • Ignoring that some downgrade-to-internal fixes are real exposure bugs

context