Sitemap

They’re concise, powerful, and tempting — but are they safe?

Data Classes in Public APIs? Think Twice Before You Do It

Kotlin’s data class makes modeling a breeze—but exposing them in your public API could be your next maintenance nightmare. Here’s why, and what to do instead.

--

Press enter or click to view image in full size
Photo by Kace Rodriguez on Unsplash

Not a Medium Member? “Read for Free”

Let’s be honest: Kotlin’s data class feels like magic.
One keyword and—boom—you get equals, hashCode, toString, copy, and destructuring. No boilerplate, no fuss. So when it comes time to expose a model in your module’s public API, it’s tempting to just use a data class.

But here’s the problem: that one-liner convenience might come back to bite you. Hard.

In this article, we’ll explore why you should be very cautious about using data class in public APIs, the hidden risks most devs overlook, and what you should consider instead.

Kotlin’s data class is a true productivity booster. You define your model like this:

data class User(val id: String, val name: String)

And Kotlin generates:

  • equals() and hashCode() — great for comparisons and collections
  • copy() — handy for immutability
  • toString() — for readable logging
  • componentN() — for destructuring

It’s the perfect recipe for DTOs, cache entries, and local models. But that’s exactly why it shouldn’t be your go-to for public APIs.

What’s the Real Danger?

1. Too Much Auto-Generated Behaviour

When you expose a data class publicly, you also expose its:

  • Property order (component1(), component2()…)
  • Equality logic (equals() uses all properties)
  • Copy semantics (deep/shallow assumptions)
  • String representation

And here’s the kicker: every one of these is now part of your API contract.

If you change anything — even property order or add a field — you risk breaking consumers who rely on destructuring, equality, or copy().

2. Binary Compatibility Risks

Kotlin generates hidden methods for data classes. Changing them — even slightly — can break ABI (Application Binary Interface) compatibility across modules or libraries.

You might think:

“It’s just internal to my library.”

But if someone is using your Kotlin code as a binary dependency (e.g., via Maven or Gradle), changes to your data class can cause:

  • NoSuchMethodError
  • IncompatibleClassChangeError
  • Serialization failures

3. Violation of Encapsulation

Data classes expose all constructor properties — publicly and by default.

There’s no hiding internal logic, lazy fields, or domain rules.

If tomorrow you decide to:

  • Add validation logic
  • Transform inputs
  • Cache computed properties

…you’ll be stuck, because everyone already depends on the raw fields and their exact names.

Suppose you published this in your library:

data class Payment(val amount: Double, val currency: String)

Six months later, product requirements change. You want to:

  • Make currency optional
  • Change amount from Double to BigDecimal
  • Add a field for payment status

Any of these can cause:

  • Destructuring breaks
  • Unexpected behaviour in .equals() or .copy()
  • Serialization mismatch if consumers rely on default constructors

Congrats, you’re now versioning a landmine.

Better Alternatives (And When to Use Them)

1. Use Regular Classes for Public Models

class Payment(
val amount: BigDecimal,
val currency: String,
val status: PaymentStatus
)

This gives you control over behaviour and evolution.

  • Override only what you need (equals, toString, etc.)
  • Add fields safely
  • Hide internal properties
  • Avoid unintended usage like destructuring

2. Use Interfaces for Contracts

Define a Payment interface and provide internal implementations that may or may not use data classes.

interface Payment {
val amount: BigDecimal
val currency: String
}

This way, you control what’s exposed and avoid coupling clients to your concrete model.

3. Restrict Visibility

If you must use a data class, mark it as internal or use it only as a private implementation detail. Wrap it in a mapper or adapter when exposing through the public API.

internal data class PaymentImpl(...) : Payment

This lets you change implementation freely, without breaking consumers.

Conclusion

Kotlin’s data class is an amazing tool—when used responsibly.

In private or internal layers? Go wild.
In public APIs? Think twice. You’re not just exposing a type — you’re exposing behaviour, structure, and future limitations.

APIs are contracts. Once released, they’re hard to change without breaking people. Use data class where it belongs—and build robust, evolvable APIs your future self will thank you for.

--

--

Jayant Kumar🇮🇳
Jayant Kumar🇮🇳

Written by Jayant Kumar🇮🇳

Jayant Kumar is a Lead Software Engineer, passionate about building modern Android applications. He shares his expertise in Kotlin, Android, and Compose.

No responses yet