skip to content

You expose a widely-used `inline fun <reified T> decode(bytes: ByteArray): T` in a public library. What are the bytecode/ABI and binary-compatibility consequences, and how would you mitigate them?

level: principalimportance: nice to knowfreq 22%

answer

  1. inline = monomorphization, body in caller bytecode
  2. old inlined copy persists until recompile
  3. thin reified shim -> stable non-inline core
  4. @PublishedApi freezes internals into ABI
  5. pass T::class to the worker function

basics

~20 s

Because it's inline, the body is copied into every place callers use it, so the real logic lands in their code. Changing the body forces recompilation and can bloat their binaries. Keep the inline part tiny and delegate the heavy work to a normal function.

solid answer

~50 s

An `inline fun <reified T> decode` is **monomorphized**: at each call site the body is copied with `T` substituted, so the implementation physically lives in consumer bytecode. Consequences: (1) code-size growth multiplied by call-site count; (2) any change to the inline body is a **source/binary-compatibility hazard** — callers keep the *old* inlined copy until recompiled, so behavioral fixes don't reach precompiled clients; (3) `internal` members touched by the body must be `@PublishedApi internal`, freezing them into your ABI; (4) Java consumers can't use it. Mitigation: keep the inline reified shim a one-liner that immediately calls a stable, non-inline worker `fun <T> decodeImpl(bytes: ByteArray, type: KClass<T>): T`, passing `T::class`. The shim is trivial and rarely changes; all real logic stays in one shared, independently-updatable method, restoring normal binary compatibility while preserving the ergonomic reified call site.

code

kotlin · 8 lines
kotlin
inline fun <reified T : Any> decode(bytes: ByteArray): T =
    decodeImpl(bytes, T::class)            // tiny, stable, just captures T

@PublishedApi
internal fun <T : Any> decodeImpl(bytes: ByteArray, type: KClass<T>): T {
    // real, evolvable, single-copy implementation
    TODO()
}

go deeper

for a junior

Understands that inline copies the body into callers and that big inline bodies bloat code.

for a middle

Names the recompilation/compatibility issue and that internals need @PublishedApi when used in inline bodies.

for a senior

Designs the thin-shim-over-stable-core pattern and explains why it restores binary compatibility while keeping reified ergonomics.

for a principal

Weighs ABI surface, monomorphization cost, Android DEX limits, and library-evolution guarantees, deciding precisely what to inline vs. keep behind a stable non-inline core.

## What 'inline reified' costs at scale `inline` performs **monomorphization**: the compiler copies the function body — with the reified `T` replaced by each concrete type — into every call site. For a public library helper called from thousands of places, that means: - **Binary bloat**: the body is duplicated per call site (and per distinct `T`). A large body multiplies fast; bytecode and method-count (relevant on Android's 64K DEX limit) grow. - **Baked-in implementation = compatibility trap**: because the logic is *in the caller's* compiled output, a downstream app that compiled against v1 still runs the **v1 inlined body** even after upgrading your jar — until it recompiles. So a bug fix or behavior change in the inline body does **not** reach precompiled clients. This is the classic reason to never put nontrivial or security-sensitive logic in a public inline function. - **ABI leakage via `@PublishedApi`**: if the body references `internal` declarations, you must mark them `@PublishedApi internal`. They now effectively become part of your stable ABI — you can't rename/remove them without breaking inlined callers. - **No Java interop / no function reference**: Java callers and reflective/`::` references can't use the reified parameter. ## The mitigation: thin shim + stable core Make the inline reified function a **near-empty adapter** whose only job is to capture `T` as a runtime value and forward to a normal function: ```kotlin // public, tiny, rarely-changing — only captures T inline fun <reified T : Any> decode(bytes: ByteArray): T = decodeImpl(bytes, T::class) // real logic: non-inline, single shared copy, freely updatable @PublishedApi internal fun <T : Any> decodeImpl(bytes: ByteArray, type: KClass<T>): T { /* ... */ } ``` - The inlined part is one line, so duplicated bytecode is negligible and it almost never changes. - `decodeImpl` is a normal method: one copy in your jar, fully **binary-compatible** to evolve, fixable for already-compiled clients. - You keep the ergonomic `decode<User>(bytes)` call site. ## When inlining the whole thing is fine If the function is genuinely tiny (a single `is T` / `T::class.java` expression) and internal to your module, full inlining is harmless — the shim pattern matters mainly for **public** APIs with substantial bodies. ## Decision summary Expose reified ergonomics at the boundary; keep behavior in a stable, non-inline core; minimize `@PublishedApi` surface; measure code size on call-site-heavy platforms (Android). This preserves both DX and long-term evolvability.

  • Why doesn't a bug fix in a public inline body reach already-compiled clients?
    Their compiled output contains the old inlined copy of the body. Until they recompile against the new version, they keep executing the stale logic — only the thin shim approach avoids this by keeping logic in a shared method.
  • What does `@PublishedApi internal` imply for your ABI?
    The marked declaration is callable from inlined bodies in other modules, so it effectively becomes part of your binary contract — renaming or removing it breaks precompiled callers.

The inline shim is a thin storefront; the warehouse (real logic) stays in one building so you can restock it without rebuilding every customer's house.

saying these in an interview costs you the question

  • Putting large/security-sensitive logic directly in a public inline body
  • Assuming upgrading the library jar updates already-inlined call sites
  • Ignoring code-size/DEX-method impact on call-site-heavy platforms
  • Exposing internal helpers without realizing @PublishedApi freezes them
  • Believing reified forces you to inline the whole implementation

context