skip to content

Your build sets a custom Gradle module name, and later a Java module that referenced a mangled `internal` method by its `$module` name fails to link. Explain the binary-compatibility hazard and how mangling factors in.

level: seniorimportance: should knowfreq 18%

answer

  1. suffix = $<module-name>, comes from build config
  2. rename module -> mangled names change -> NoSuchMethodError
  3. source unchanged but ABI breaks (binary compat)
  4. internal names are intentionally unstable = not API
  5. value-class -hash is type-derived, deterministic

basics

~20 s

The suffix on an internal method contains the module's name. If the module name changes, the mangled method name changes too, so anything that linked against the old name breaks. Internal members are not stable API — don't rely on their JVM names.

solid answer

~50 s

Internal members are emitted as `name$<module-name>`. The `<module-name>` part is derived from the compilation module name (configurable via the compiler's `-module-name` / Gradle `compilerOptions.moduleName`). If you rename the module or change that option, every internal member's mangled name changes, breaking any pre-compiled caller (Java code, or a library that was built against the old bytecode) that referenced it by the mangled name. This is a **binary-compatibility** hazard: the Kotlin source is unchanged but the ABI shifted. The lesson: the `$module` suffix is intentionally unstable — internal members are not part of the binary API surface and must not be linked against from outside the module. If a member genuinely needs to be Java-callable and stable, make it `public` (optionally with `@JvmName`) so it has a deterministic, supported name. Value-class `-<hash>` suffixes are deterministic from the types but still not a supported public ABI.

go deeper

for a junior

Recognizes the suffix contains the module name and that it can change.

for a middle

Connects a module rename to a changed symbol and a possible NoSuchMethodError.

for a senior

Frames it as an ABI/binary-compatibility issue, knows source is unchanged, and distinguishes module-derived vs type-derived suffixes.

for a principal

Sets policy: internal = non-API, promote to public+@JvmName when Java needs it, and configures ABI validation to ignore mangled members.

## How the suffix is built For `internal` members the compiler appends `$<module-name>`. The module name defaults from the build (e.g. Gradle source set → `app_main`) and can be overridden: ```kotlin // build.gradle.kts kotlin { compilerOptions { moduleName.set("payments") } } ``` Now `internal fun computeScore()` becomes `computeScore$payments` instead of `computeScore$app_main`. ## The binary-compatibility hazard ABI (Application Binary Interface) compatibility means **already-compiled** callers keep linking without recompilation. The JVM resolves methods by name + descriptor at link time. If a caller (a Java class, or a downstream jar) was compiled against `computeScore$app_main` and you rename the module to `payments`, the symbol it imports no longer exists → `NoSuchMethodError` at runtime (or a compile failure if recompiled against new headers). Crucially **the Kotlin source did not change** — only the build configuration did. This is the trap: a 'harmless' module rename is a breaking ABI change for anything that reached into internal members. ## Why this is by design Kotlin deliberately makes internal-member names **unstable** so they signal 'not API'. You are *not supposed* to link against them across module/build boundaries. The instability is the enforcement mechanism, complementing the visibility check that Kotlin (but not Java) honors. ## Value classes differ Value-class mangling (`-<hash>`) is derived from the **types** in the signature, not the module name, so it's deterministic across builds. But it's still not a supported public ABI you should hand-code against from Java; use `@JvmName` to get a real name. ## Practical guidance - Never call a `$module`-mangled method from Java in another module; treat it as private. - If Java must call it: promote to `public` (and `@JvmName` if you want a chosen name) so the name is part of the maintained API. - Pin or avoid churn in `moduleName` if, for legacy reasons, something does depend on internal names — but the real fix is to stop depending on them. - Tools like binary-compatibility-validator track `public`/`protected` ABI; they intentionally ignore internal/mangled members, reinforcing that those aren't API. ## Key takeaway The module-name suffix encodes a build-time identity into the symbol; changing that identity rewrites symbols. That's a feature that enforces 'internal is not API', not an accident to work around.

  • Would changing the module name also affect value-class `-<hash>` suffixes?
    No. Value-class mangling is derived from the parameter/return types, not the module name, so it stays stable across a module rename.
  • How should a binary-compatibility validator treat mangled internal members?
    It should ignore them — they aren't public ABI. Validators track `public`/`protected` surface only, reinforcing that internals must not be linked against.

The suffix is like a return address tied to your current office; move offices and old mail bounces — that's the point, internal mail isn't meant to leave the building.

saying these in an interview costs you the question

  • Treating mangled internal method names as a stable contract
  • Claiming a module rename can never be a breaking change
  • Confusing the type-derived value-class hash with the module-derived internal suffix
  • Suggesting you 'just hardcode the mangled name' in Java as a permanent fix

context