You're designing a Kotlin library consumed by both Kotlin and Java teams, using `@JvmInline value class` domain types and `internal` helpers. How do you architect the public surface so mangling never bites Java consumers?
answer
- Java only meets public, unmangled, stable signatures
- internal -> public facade, never call mangled internals
- value class -> underlying-type overload for Java
- @get:JvmName for leaked value-class getters
- CI: bin-compat validator + Java consumer smoke test
basics
~20 sKeep the value classes for Kotlin callers, but give Java a separate, plain-typed public API. Don't let Java touch internal members or value-class-typed methods directly; expose facade functions that take primitive/boxed types and have stable names.
solid answer
~40 sTreat the Java-facing surface as a deliberately designed layer. (1) Never expect Java to call `internal` members — they're `$module`-mangled and ABI-unstable; if Java needs the logic, expose a `public` facade. (2) For functions touching `@JvmInline value class` types, Java sees `-<hash>` names it can't type; provide either `@JvmName`-annotated entry points (verifying no platform clash) or **overloads that take the underlying type** (`fun amount(cents: Long)`), so Java never handles the wrapper. (3) Use property accessor control (`@get:JvmName`) where value-class-typed properties leak. (4) Run Kotlin/Java consumer smoke tests and a binary-compatibility validator (which ignores mangled members) in CI. (5) Document that value classes are a Kotlin-only ergonomic; Java gets primitives. The goal: Java consumers only ever see stable, unmangled, public signatures — mangling stays an internal implementation detail.
code
kotlin · 11 lines@JvmInline value class Money(val cents: Long)
// Kotlin-idiomatic (mangled to charge-<hash> in bytecode)
fun charge(amount: Money) = chargeCents(amount.cents)
// Stable Java-facing facade: plain Long, unmangled name
fun chargeCents(cents: Long) { /* core logic */ }
// Leaked value-class getter -> give Java a clean accessor
val Account.total: Money get() = Money(0)
val Account.totalCents: Long get() = total.centsgo deeper
Understands Java should call plain, public methods, not mangled ones.
Adds underlying-type overloads and @JvmName to expose value-class logic to Java.
Separates internal helpers from a public facade and controls property-accessor leaks with @get:JvmName.
Designs the whole Java surface as a stable primitive-typed layer, backs it with ABI validation + Java consumer tests, and documents value classes as Kotlin-only ergonomics.
## Framing Mangling is invisible to Kotlin callers but a wall for Java. Library design must ensure **Java only meets unmangled, stable, public signatures**. Two mangling sources to neutralize: `internal` members (`$module`) and value-class signatures (`-<hash>`). ## Principle 1 — internals are not API `internal` helpers exist for Kotlin module-internal reuse. Their JVM names are module-derived and unstable. **Never** ask Java to call them. If Java needs the behavior, add a `public` function (a facade) that delegates internally. This keeps Kotlin visibility honest and gives Java a maintained symbol. ## Principle 2 — value classes are a Kotlin ergonomic `@JvmInline value class Money(val cents: Long)` erases to `Long`, and any function using it gets a hyphenated name Java can't reference. Options, in order of preference: ```kotlin @JvmInline value class Money(val cents: Long) // Kotlin-facing, idiomatic fun charge(amount: Money) { chargeCents(amount.cents) } // Java-facing overload using the underlying type — no wrapper, stable name fun chargeCents(cents: Long) { /* core logic */ } ``` - **Underlying-type overload** (best): Java never sees `Money` at all. - **`@JvmName` entry point**: gives a clean name but you must guarantee no platform-declaration clash with the underlying-type overload. - **Property accessors**: a `val total: Money` getter is mangled; use `@get:JvmName("getTotalCents")` or expose `val totalCents: Long`. ## Principle 3 — guard with tooling - A **binary-compatibility validator** dumps the `public`/`protected` ABI; it ignores mangled internal/value-class members, so reviewing its output confirms what Java actually sees. - Add a small **Java consumer module in tests** that compiles against the library — if Java can't reach an intended API, the mangling leaked and the build fails fast. ## Principle 4 — documentation and intent State clearly: value classes are zero-cost types for Kotlin; Java callers use primitive overloads. This prevents Java teams from hand-coding hyphenated names (which they can't type anyway) or depending on internal `$module` symbols. ## Edge cases - **Unsigned types** (`UInt`, `ULong`) are value classes too → same mangling; avoid them in Java-facing signatures. - **Nullable value class** (`Money?`) forces boxing and changes the signature/mangling — be deliberate. - **Multi-field value classes** (newer Kotlin) erase differently; re-check the Java view. ## Key takeaway Don't fight mangling at the call site; **design it out** of the Java surface. Mangled names should only ever appear behind public, primitive-typed, stably-named facades.
- Why prefer an underlying-type overload over `@JvmName` for Java consumers of a value-class function?The overload keeps Java free of the wrapper entirely and avoids platform-declaration-clash risk; `@JvmName` removes the safety net and Java still conceptually deals with the erased type.
- How can CI catch a leaked mangled name before release?Compile a small Java consumer module against the library in tests, and run a binary-compatibility validator — both surface whether intended APIs are reachable and unmangled.
Build a clean lobby (public primitive API) for Java visitors; keep the value-class plumbing and internal corridors behind staff doors they never need to open.
saying these in an interview costs you the question
- Exposing `@JvmName`-ed internals as the Java API instead of real public facades
- Forgetting that unsigned types are value classes and also mangle
- Assuming nullable/multi-field value classes erase identically to single-Long ones
- No Java-consumer or ABI test to detect leaks