Kotlinās Evolution Just Made Your āclassā Obsolete
Why I Stopped Using class in Kotlin (And What I Use Instead)
Discover why data, object, value, and sealed are quietly replacing the traditional classāand how they can clean up your Kotlin code for good.
Not a Medium Member? āRead for Freeā
When I first started writing Kotlin, I used class like I did in Javaābecause thatās what I knew.
class User(val name: String, val age: Int)Simple, right? But over time, I started noticing something odd:
I wasnāt really using plain class anymore.
Instead, I was gravitating toward:
data classfor modelsobjectfor singletonssealed classfor state managementvalue classfor wrappersinterfacewith default methods for behaviour
And soon, class started to feel⦠unnecessary.
This wasnāt just a style preference ā it was a productivity boost, a readability win, and a bug-preventing habit.
In this article, Iāll show you why I stopped using class in most of my Kotlin code, and what you should be using instead.
1. Use data class for Models ā Itās Just Smarter
If your class is just holding data, like most models do, youāre wasting time using regular class.
// Donāt do this
class User(val name: String, val age: Int)Youāll miss out on:
- Auto-generated equals(), hashCode(), toString(), and copy()
- Structural equality checks
- Easy destructuring
Instead, use:
data class User(val name: String, val age: Int)This alone reduces boilerplate and improves correctness.
2. Use object for Singletons ā No More Manual Patterns
You donāt need to write Singleton boilerplate like this anymore:
class Logger private constructor() {
companion object {
val instance = Logger()
}
}Instead, just use:
object Logger {
fun log(msg: String) = println(msg)
}Itās cleaner, safer, and Kotlin-native. object handles thread-safety and initialization out of the box.
3. Use sealed class for UI State and Navigation
Need polymorphism without accidental inheritance? A regular class is risky.
Use sealed to tightly control your type hierarchy:
sealed class ApiState<out T> {
data class Success<out R>(val data: R) : ApiState<R>()
data class Failure(val msg: String) : ApiState<Nothing>()
data object Loading : ApiState<Nothing>()
}This gives you:
- Exhaustive
whenstatements - Compile-time safety
- Clearer domain modeling
4. Use value class (JVM inline class) for Type Safety Without Overhead
Say you have something like this:
class Email(val value: String)It adds type safety, but at a runtime cost. Why create an object just to wrap a primitive?
Instead, use @JvmInline value class:
@JvmInline
value class Email(val value: String)Now, you get type safety and zero runtime allocation. Itās great for IDs, wrappers, and primitives-as-types.
5. Use interface With Default Methods for Composition
Prefer composition over inheritance? Kotlin makes this clean:
interface Clickable {
fun onClick() = println("Clicked!")
}Then mix it into your class:
class Button : ClickableNo need for abstract base classes just to reuse behaviour.
To be fair, class isnāt dead. There are still a few cases where it makes sense:
- When building a mutable state holder (e.g.,
ViewModel) - When extending Java classes that require open inheritance
- When you need non-final classes for testing (though this can often be avoided)
But these are the exceptions, not the rule.
Conclusion
Learning Kotlin isnāt just about syntax ā itās about adopting a mindset that values clarity, safety, and brevity.
Letting go of class in favour of Kotlinās modern constructs helped me:
- Write more expressive code
- Reduce boilerplate
- Avoid bugs
- Think more functionally
So the next time you reach for class, stop and ask:
āIs there a better Kotlin-native way to model this?ā
Chances are, there is. And once you get used to it, you wonāt look back.
