As a senior, when would you mandate explicit return types on single-expression functions across a codebase, and how does this interact with overrides and binary compatibility?
answer
- Inferred type = accident of current body
- Refactor can silently change return type → source/binary break
- explicitApi() / -Xexplicit-api=strict enforces it
- Public/protected only; private can infer
- Overrides take type from base declaration
basics
~20 sFor public or library code, require explicit return types so the type is a deliberate promise that won't change by accident. For small private helpers, inference is fine. Explicit types also make overrides and shared APIs safer and more stable.
solid answer
~40 sI mandate explicit return types on every public/protected single-expression function — typically by enabling `-Xexplicit-api=strict` (the `explicitApi()` DSL) for libraries and published modules. Rationale: an inferred return type is an *accident of the current implementation*; refactoring the body can silently widen/narrow it, breaking source and even binary compatibility for downstream consumers. Explicit types make the return a contract that the compiler enforces. Overrides inherit the supertype's declared type, so the concern is mainly at the declaration site of the API surface. For internal/private helpers I allow inference to keep code terse. The policy balances brevity (fast-moving internal code) against stability (public API). Pair it with a CI rule so violations fail the build rather than relying on review discipline.
code
kotlin · 7 lines// build.gradle.kts
kotlin {
explicitApi() // strict: public/protected decls must declare return types & visibility
}
// now this public single-expression function must declare : List<String>
fun roles(): List<String> = listOf("admin", "user")go deeper
Understands that public functions should declare their return type for clarity.
Explains that inferred types can change with the body and that public APIs should be explicit.
Sets a policy distinguishing public vs internal code and enforces it via explicit-api mode in CI.
Weighs binary-compatibility and ecosystem impact, codifying the rule across modules and documenting the rationale for the org.
## The core risk: inferred types are not contracts An expression-body function's inferred type reflects *today's* implementation: ```kotlin // v1 — inferred return type is List<String> (returns listOf) fun roles() = listOf("admin") // v2 — someone refactors; now inferred MutableList<String> fun roles() = mutableListOf("admin") ``` Downstream code compiled against v1 may break: the *source* contract shifted and, for library jars, the **binary signature** (method descriptor / return type in the bytecode) changed, risking `NoSuchMethodError`-class incompatibilities. Declaring `fun roles(): List<String>` freezes the contract regardless of body changes. ## explicitApi() / -Xexplicit-api Kotlin offers an **explicit API mode** for libraries: - `explicitApi()` (or `explicitApiWarning()`) in the Kotlin Gradle DSL, equivalently `-Xexplicit-api=strict|warning`. - It **requires** explicit visibility and explicit return types on all *public* and *protected* declarations, including single-expression functions. - Strict mode fails the build; warning mode reports. This moves the policy from review etiquette to a compiler-enforced gate. ## Overrides An `override fun` must conform to the supertype's declared return type (Kotlin allows covariant return types). The type is governed by the **base declaration**, so as long as the API surface (interface/abstract class) declares explicit types, overrides are constrained correctly. You can still write the override as an expression body: ```kotlin interface Repo { fun find(id: Long): User? } class JpaRepo : Repo { override fun find(id: Long) = em.find(User::class.java, id) // type comes from Repo } ``` ## Where I draw the line - **Public API / libraries / published modules:** explicit return types, enforced by explicit-api mode in CI. - **internal/private application code:** inference allowed for brevity; the blast radius of a changed inferred type is the module only. ## Tying it to the gate Put the setting in the build so it fails CI; don't rely on humans spotting a missing type in review. This is the difference between a *convention* and an *enforced contract*.
- How can changing a single-expression function's body break binary compatibility?If the return type was inferred, a body change can change the inferred type, which alters the method's return-type descriptor in the bytecode; callers compiled against the old descriptor can hit linkage errors.
- Do overrides need their own explicit return type under explicit-api mode?Overrides inherit the type from the base declaration, so explicit-api focuses on the declaring API surface; the override can still be an expression body whose type is fixed by the supertype.
saying these in an interview costs you the question
- Treating an inferred public return type as a stable contract
- Unaware of explicitApi()/-Xexplicit-api for libraries
- Believing body refactors can never affect a function's signature
- Applying strict explicit-api to throwaway internal code, harming velocity
- Relying solely on code review instead of a CI-enforced gate