Kotlin Value Classes
A value class wraps a single value in a named type. At runtime, the JVM uses the underlying type directly — no object allocation. This gives you type safety and clear intent without any performance cost. Value classes prevent common bugs like accidentally swapping a user ID for an order ID.
The Problem They Solve
Without value classes — primitives carry no meaning:
fun processOrder(userId: Int, orderId: Int, productId: Int) { ... }
processOrder(orderId, userId, productId) // oops! wrong order — compiles fine
With value classes — types enforce correct usage:
fun processOrder(userId: UserId, orderId: OrderId, productId: ProductId) { ... }
processOrder(orderId, userId, productId) // compile error! wrong types
Defining a Value Class
@JvmInline
value class UserId(val id: Int)
@JvmInline
value class OrderId(val id: Int)
@JvmInline
value class Email(val address: String) {
init {
require(address.contains("@")) { "Invalid email: $address" }
}
val domain: String get() = address.substringAfter("@")
}
fun main() {
val uid = UserId(42)
val oid = OrderId(42)
println(uid.id) // 42
println(oid.id) // 42
println(uid == oid) // false — different types even though both hold 42
val email = Email("alice@example.com")
println(email.domain) // example.com
}Value Class Rules
✓ Must be annotated with @JvmInline (on JVM)
✓ Must have exactly ONE val in the primary constructor
✓ Can have properties (computed only — no backing fields)
✓ Can have member functions
✓ Can implement interfaces
✗ Cannot extend classes (but can implement interfaces)
✗ Cannot have a backing var property
✗ Cannot have init that stores state
Zero Boxing at Runtime
// At source level:
val id = UserId(42) // looks like an object
// At runtime (JVM bytecode):
val id = 42 // just an Int — no object created!
// Exception: when stored in a nullable or Any variable,
// boxing happens as usual.
val boxed: UserId? = UserId(42) // boxed here (nullable)
val generic: Any = UserId(42) // boxed here (Any)
Value Class with Interface
interface Displayable {
fun display(): String
}
@JvmInline
value class Temperature(val celsius: Double) : Displayable {
val fahrenheit: Double get() = celsius * 9 / 5 + 32
val kelvin: Double get() = celsius + 273.15
override fun display() = "${"%.1f".format(celsius)}°C / ${"%.1f".format(fahrenheit)}°F"
operator fun plus(other: Temperature) = Temperature(celsius + other.celsius)
operator fun minus(other: Temperature) = Temperature(celsius - other.celsius)
}
fun main() {
val body = Temperature(37.0)
val room = Temperature(22.0)
val diff = body - room
println(body.display()) // 37.0°C / 98.6°F
println(room.display()) // 22.0°C / 71.6°F
println("Difference: ${diff.celsius}°C")
}Practical Example: Type-Safe IDs
@JvmInline value class CustomerId(val value: Long)
@JvmInline value class ProductId(val value: Long)
@JvmInline value class InvoiceId(val value: Long)
data class Invoice(
val id: InvoiceId,
val customerId: CustomerId,
val productId: ProductId,
val amount: Double
)
fun generateInvoice(customerId: CustomerId, productId: ProductId, amount: Double): Invoice {
val invoiceId = InvoiceId(System.currentTimeMillis())
return Invoice(invoiceId, customerId, productId, amount)
}
fun main() {
val cid = CustomerId(1001L)
val pid = ProductId(5042L)
// generateInvoice(pid, cid, 999.0) ← compile error: types are swapped
val invoice = generateInvoice(cid, pid, 1499.0)
println("Invoice ${invoice.id.value}")
println("Customer ${invoice.customerId.value}")
println("Product ${invoice.productId.value}")
println("Amount ₹${invoice.amount}")
}