skip to content

A library exposes a public `const val VERSION = "1.0"`. The maintainer bumps it to `"1.1"` and republishes only the library JAR. Why might consumers still see `"1.0"`, and how does this differ from a plain `val`?

level: seniorimportance: should knowfreq 35%

answer

  1. const is inlined into caller's constant pool
  2. Library JAR swap doesn't update baked-in consumers
  3. Recompile consumers to pick up new const
  4. Plain val = runtime getter, JAR swap suffices
  5. Same hazard as Java public static final

basics

~20 s

Because const values get copied directly into the code that uses them at compile time. Consumers that were already compiled still carry the old copied-in value until they are recompiled. A plain val is read fresh from the library each run, so updating the library is enough.

solid answer

~40 s

`const val` is **inlined** at every use site: the consumer's bytecode embeds the literal `"1.0"` (in its constant pool) rather than reading the library's field. Republishing only the library JAR does not touch already-compiled consumer bytecode, so they keep `"1.0"` until they are **recompiled** against the new JAR. This is a binary-compatibility hazard: a `const val` is effectively part of the consumer's compiled code. A plain `val` (or a function) is accessed through a **getter / field read** at runtime, so the consumer fetches the current value from the loaded library — swapping the JAR alone updates everyone without recompilation. The trade-off: `const` gives zero-overhead reads and works in constant-expression contexts (annotations), but you sacrifice the ability to change the value without forcing consumer recompiles.

code

kotlin · 7 lines
kotlin
// LIBRARY v1
const val VERSION = "1.0"        // inlined into consumers
val featureName = "alpha"        // getter-backed, read at runtime

// After bumping library only (no consumer recompile):
//   VERSION at consumer -> still "1.0" (baked in)
//   featureName at consumer -> reflects new value (read from loaded JAR)

go deeper

for a junior

Recognizes const is fixed at compile time but may not articulate the cross-JAR recompilation consequence.

for a middle

Explains that the value is inlined into consumers and that recompilation is needed to pick up changes.

for a senior

Frames it as a binary/ABI-compatibility hazard, contrasts getter-backed val, and gives concrete API-design guidance.

for a principal

Sets a policy: which constants belong as const vs getter-backed on public APIs, considers incremental-compilation tracking and the Java static final parallel for mixed codebases.

## The mechanism: inlining When you reference a `const val`, the Kotlin compiler does not emit a field read. It **copies the literal value** into the calling code: ```kotlin // library const val VERSION = "1.0" // consumer, compiled against the library fun log() = println(VERSION) // bytecode literally embeds "1.0" ``` The consumer's `.class` file stores `"1.0"` in its own **constant pool**. There is no link back to the library's field at runtime for that read. ## Why the bump doesn't propagate Bumping the library to `VERSION = "1.1"` and shipping only the library JAR changes the library's bytecode. But the **consumer was already compiled** with `"1.0"` baked in. Nothing re-reads the library, so the consumer prints `"1.0"` until **it** is recompiled against the new JAR. This is a classic **binary (ABI) compatibility** pitfall — the constant is part of the consumer's compiled output, not a runtime dependency. ## Contrast with a plain `val` ```kotlin val version = "1.0" // read-only property, getter-backed ``` A `val` compiles to a backing field plus a synthesized **getter** (e.g. `getVersion()`). The consumer calls that getter / reads that field **at runtime**, fetching whatever value the currently loaded library holds. Replace the JAR and every consumer sees the new value with no recompilation. Same is true for a function returning the value. ## Practical guidance - Use `const val` for values that are **conceptually frozen** and cheap to inline (math constants, fixed protocol numbers, keys that must be compile-time constants for annotations). - For values on a **public API surface** that may evolve (versions, configurable defaults, feature names), prefer a plain `val` or a getter so you retain the freedom to change them without forcing consumer recompiles. - This is analogous to Java's `public static final` constants, which have the **same** inlining behavior — `const val` is its Kotlin equivalent. ## Secondary effects - Because the value is duplicated into every caller, a `const` that ends up unused in a caller still left its literal there at compile time (no runtime cost, but the source dependency on the value's exact text existed at build time). - Tooling like incremental compilation generally tracks `const` references and recompiles dependents, but **prebuilt** artifacts (already-shipped JARs) are not retroactively updated.

  • Does a plain `val` with a custom getter have the same inlining hazard?
    No. A custom getter is a method call evaluated at runtime against the loaded library, so consumers always see the current behavior after a JAR swap — no recompile needed.
  • Is this Kotlin-specific?
    No — Java's `public static final` String/primitive constants are inlined identically by javac. `const val` is the Kotlin equivalent and inherits the same binary-compatibility caveat.

const is like printing a phone number on your own business cards — reprinting the company directory doesn't change the cards already handed out. A val is like writing 'call the front desk' — the desk always has the current number.

saying these in an interview costs you the question

  • Claiming the library JAR swap always updates consumers for `const`
  • Not knowing `const` is inlined into the consumer's bytecode
  • Confusing this with a runtime caching problem rather than compile-time inlining
  • Believing a plain `val` has the same recompile requirement
  • Recommending `const` for evolving public API values without caveats

context