Kotlin API Design That Ages Well: What Your Interfaces Won't Tell You
Stable kotlin api design breaks in a specific way: not at the commit where the problem was introduced, but weeks later, in someone else's module, on a release nobody planned for. The code looked right. The diff was clean. But the real problem with Kotlin API Design is the gap between what the source shows you and what the JVM actually executes — and that's exactly where public contracts quietly fall apart.
Default Arguments and the Bitmask You Never Wrote
Kotlin default arguments binary compatibility is one of those issues that feels impossible until you see it once. Under the hood, the compiler doesn't just emit the function you wrote — it also generates a synthetic $default overload that reconstructs missing arguments using a bitmask. That overload has its own JVM signature. Add a new optional parameter, and the signature changes. Modules that compiled against the old version now reference a method that doesn't exist. The result is NoSuchMethodError at runtime in code that nobody touched, on a binary that was never rebuilt. Source-compatible change, binary-incompatible outcome — and one of the most common kotlin library design pitfalls in multi-module projects where not everything recompiles on every change.
Extension Functions Don't Behave Like Methods
The readability of extension functions comes with a hidden cost. Callers read them like instance methods and assume override semantics, polymorphic dispatch, predictable behavior on subclasses. None of that is true. Kotlin extension functions pitfalls start with static resolution — the compiler picks the implementation at compile time based on the declared type, not the runtime type. Two conflicting extensions from different modules resolve silently based on import order. And if the type you're extending ever ships a real member function with the same name, your extension gets shadowed permanently, with no warning anywhere in the build. From a kotlin public api design perspective, extensions that carry real business logic in a public surface are a liability — the right tool is an interface or a typed wrapper.
Sealed Classes Lock Every Consumer Into Your Hierarchy
The sealed class vs interface kotlin api decision usually gets made in thirty seconds for the wrong reasons. Sealed feels cleaner, exhaustive when expressions look great, the compiler enforces coverage. What that guarantee actually means for consumers: the moment you add a new subclass in a minor release, every exhaustive when across every downstream module stops compiling. That's a hard kotlin api breaking change even when the new variant is purely additive on your side. The trade-off in kotlin api evolution is real — sealed is a closed-world assumption that optimizes for today and accumulates cost every time the hierarchy needs to grow. Exposing an interface publicly while keeping sealed internals gives you exhaustive dispatch where it matters without committing consumers to a hierarchy they can't extend.
What the JVM Sees That Kotlin Hides
Kotlin binary compatibility issues rarely come from removing things. They come from changes that look structural at the source level but silently alter bytecode signatures. A val becoming var adds a setter method. Reordering data class constructor parameters shifts every componentN() function. Marking a function inline means the function body gets copied into every call site at compile time — update the implementation in a new release, and older callers keep running the version they compiled against. Companion object accessors, copy(), default parameter factories — these are all compiler-generated JVM methods with no direct representation in Kotlin source. Kotlin backward compatibility issues almost always trace back to treating the source diff as the full picture of what changed, when the actual contract lives one layer down.
Tooling that actually catches this: kotlinx-binary-compatibility-validator generates a .api file on each release and makes ABI changes visible in PR diffs. Without it, kotlin api best practices around version stability are essentially unenforceable — you're reviewing an abstraction while the ABI changes underneath it.
Top comments (0)