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}")
}

Leave a Comment

Your email address will not be published. Required fields are marked *