When designing a Kotlin library API consumed by Java, how do you decide between @JvmOverloads, explicit overloads, and a builder — and what binary-compatibility tradeoffs matter?
answer
- Trailing & few → @JvmOverloads
- Non-trailing/divergent → explicit overloads
- Many growing optionals → builder
- Generated overloads are binary API
- Kotlin side already solved by named+default args
basics
~20 sUse @JvmOverloads for simple trailing-optional parameters where Java just wants to omit the last few. Use explicit overloads when you need non-trailing combinations or different bodies. Use a builder when there are many independent optional fields. Remember each generated overload becomes part of your binary API.
solid answer
~40 sDecide by shape of optionality and stability needs. @JvmOverloads is ideal when optionals are few and naturally trailing, and you want zero boilerplate; the cost is that every generated overload is a committed public/binary signature — reordering or removing parameters later breaks Java callers at the bytecode level. Explicit overloads suit cases needing non-trailing combinations, divergent behavior, or finer control over which signatures are public. A builder (or a config/options object) scales best when there are many independent optionals, avoids combinatorial overload explosion, and evolves additively without breaking existing call sites. From Kotlin's own side, named + default arguments already cover everything, so the question is purely about the Java consumer ergonomics and your library's binary-compatibility contract. Document and freeze the chosen surface; treat @JvmOverloads-generated methods as first-class public API in compatibility checks.
code
kotlin · 8 lines// Few trailing optionals: @JvmOverloads is enough
@JvmOverloads
fun open(path: String, readOnly: Boolean = false, bufferKb: Int = 8) {}
// Many independent optionals: prefer a builder for additive evolution
class HttpClient private constructor(/* ... */) {
class Builder { var timeoutMs = 5000L; var retries = 3; /* ... */ }
}go deeper
Knows @JvmOverloads exists and reduces boilerplate for Java callers.
Can pick @JvmOverloads vs explicit overloads for simple cases.
Weighs builder vs overloads and recognizes the right-to-left constraint.
Frames the choice as binary-compatibility/API-governance, plans for additive evolution, and integrates compatibility tooling.
## The decision axes When a Kotlin library must serve **Java** callers, picking how to express optional parameters is an API-governance decision, not just a syntax choice. Weigh: - **Shape of optionality** — are optionals naturally trailing, or do callers need arbitrary combinations? - **Count of optionals** — a few vs. many independent ones. - **Behavioral divergence** — do some combinations need different logic? - **Binary-compatibility commitment** — every public JVM signature you ship is a contract. ## Option 1: @JvmOverloads Best when optionals are **few and trailing** and you simply want Java to omit the last few. One annotation produces the right-to-left ladder. ```kotlin @JvmOverloads fun connect(host: String, port: Int = 443, timeoutMs: Long = 5000) {} ``` **Cost:** each generated overload is a **public, binary-visible method**. Reordering parameters or removing a default later is a **binary-incompatible** change — existing compiled Java callers break. So you've committed to that parameter order. ## Option 2: Explicit overloads Write the exact JVM signatures you want by hand. Use when you need **non-trailing combinations**, **divergent bodies**, or want to keep some signatures out of the public surface. More boilerplate, but precise control. Watch for clashes if you also use @JvmOverloads on a sibling. ## Option 3: Builder / options object For **many independent optionals**, a builder avoids combinatorial overload explosion and is the most **evolution-friendly**: adding a new optional is purely additive and never breaks callers. ```kotlin class Request private constructor(/* ... */) { class Builder(val url: String) { var method: String = "GET" var timeoutMs: Long = 5000 fun build(): Request = TODO() } } ``` Kotlin callers can still use a DSL/apply; Java callers get fluent chaining. ## Kotlin side is already solved Kotlin's **named arguments + default values** make all of this unnecessary for Kotlin callers — this entire question exists only because Java can't use Kotlin defaults. So optimize for the **Java consumer** and your compatibility contract. ## Binary-compatibility checklist - Treat @JvmOverloads-generated methods as **public API** in tools like binary-compatibility-validator. - Don't reorder or insert defaulted parameters in the middle once shipped. - Prefer builders when you expect the option set to grow. - Document the supported surface explicitly. ## Rule of thumb Few trailing optionals → @JvmOverloads. Need specific/non-trailing shapes or different behavior → explicit overloads. Many growing optionals → builder/options object.
- Why are @JvmOverloads-generated methods a binary-compatibility concern?They are real public JVM signatures compiled into callers; removing or reordering parameters later breaks already-compiled Java consumers.
- When does a builder beat @JvmOverloads?When there are many independent optional fields — builders avoid overload explosion and evolve additively without breaking existing call sites.
@JvmOverloads is a fixed combo menu; a builder is an à la carte order pad — the menu is fine for a few combos but the pad scales to endless choices.
saying these in an interview costs you the question
- Treating @JvmOverloads as free with no API-surface cost
- Ignoring binary compatibility when parameters change
- Recommending @JvmOverloads for many independent optionals (overload explosion)
- Forgetting Kotlin callers already have named/default args
- Not considering tooling that validates public API