Kotlin value classes
A value class gives an existing value a new, meaningful Kotlin type. A user ID and an order ID may both be strings, but Kotlin should stop you from accidentally passing one where the other is required.
@JvmInline
value class UserId(val value: String)
Mental model
Think of it as a label that matters to the compiler. UserId and OrderId may both contain a String, but they are different types and cannot be mixed accidentally.
Allow only valid values
@JvmInline
value class UserId private constructor(val value: String) {
companion object {
fun parse(raw: String?): UserId? = raw
?.trim()
?.takeIf { it.matches(Regex("U-[0-9]+")) }
?.let(::UserId)
}
}
The private constructor stops other code from creating an unchecked UserId. Callers must use parse, which trims the input and accepts only values matching the required format.
Android example
Routes, SavedStateHandle, Bundles, databases, and network models usually carry plain strings or numbers. When that value enters your feature, validate it and convert it to UserId. The rest of the feature can then use a meaningful type instead of repeatedly checking a raw string.
private val userId: UserId =
UserId.parse(savedStateHandle.get<String>("userId"))
?: error("Missing or invalid userId")
If this value is missing because of a developer mistake in an internal route, failing immediately may reveal the bug. A public deep link is different: users and other apps can send invalid input, so show an error or safe fallback instead of crashing.
What happens at runtime
Kotlin can sometimes use the wrapped String directly without creating a separate UserId object. In other situations it must create a wrapper object; this is called boxing.
| Situation | Typical behavior |
|---|---|
Concrete non-null UserId | Often represented by the underlying String |
UserId? | May be boxed |
Generic List<UserId> | Often boxed on the JVM |
| Used through an interface | Boxed |
| Java/reflection boundary | May be boxed and have name-mangled signatures |
Value classes can reduce allocations, but “zero allocation” is not guaranteed. Benchmark measured hot paths instead of inferring performance from source syntax.
A value class wraps one value to create a distinct type, such as UserId around String. Its main benefit is type safety and clearer APIs. It may avoid allocation in some JVM cases, but nullable, generic, interface, Java, and reflection usage can require boxing.
Value class versus alternatives
- Type alias: another name, but no distinct type safety.
- Data class: structural model that can store multiple values.
- Enum: fixed set of constant instances.
- Sealed type: closed family of variants with different states or data.
- Value class: meaningful single-value type, often with lightweight representation.
Interview check
Does a value class automatically guarantee valid values?
No. The value is guaranteed to be valid only when all creation goes through validation, such as a private constructor plus a checked factory function.