Value Classes in Kotlin

Value classes are compile-time type safety wrappers that can be zero-cost when inlined but incur boxing overhead in many real-world scenarios. They wrap a single property and provide strong typing without runtime overhead in specific contexts.

Basic Declaration

import kotlin.jvm.JvmInline

@JvmInline
value class UserId(val id: String)  // Property can have any name
value class Distance(val meters: Double)
value class Email(val address: String)

// Type-safe API
fun processUser(userId: UserId) { }
fun calculateSpeed(distance: Distance, time: Double): Double {
    return distance.meters / time  // Inlined: no wrapper object
}

Requirements:

  • Single val property in primary constructor
  • @JvmInline annotation for JVM targets
  • Cannot inherit from classes or be inherited (can implement interfaces)
  • Automatically generates equals() and hashCode() based on wrapped value

Advanced Features

Initialization and Validation

import kotlin.jvm.JvmInline

@JvmInline
value class Temperature(val celsius: Double) {
    init {
        require(celsius >= -273.15) { "Temperature below absolute zero!" }
    }
    
    val fahrenheit: Double get() = celsius * 9/5 + 32
    val kelvin: Double get() = celsius + 273.15
}

Interface Implementation

interface Identifiable {
    fun toIdString(): String
}

@JvmInline
value class OrderId(val value: Long) : Identifiable {
    override fun toIdString() = "ORDER-$value"
    
    companion object {
        fun parse(idString: String): OrderId {
            return OrderId(idString.removePrefix("ORDER-").toLong())
        }
    }
}

When Zero-Cost vs When Costly

Zero-Cost (Inlined) Scenarios

  • Direct property access
  • Local variables
  • Function parameters and return types (non-generic)
  • Private properties
fun processDistance(d: Distance): Double {
    return d.meters * 2  // Compiled as: d * 2 (no wrapper)
}

val speed = Distance(100.0)  // Local variable - inlined

Boxing (Costly) Scenarios

  • Generic type parameters: List<Distance>, Set<UserId>
  • Nullable types: Distance?, UserId?
  • Interface/Any types: Any, Comparable<T>
  • Java interoperability: When called from Java code
  • Reflection contexts: When accessed via reflection
  • Extension receivers on interfaces: When value class implements interface

Key Benefits

1. Type Safety

@JvmInline value class CustomerId(val id: Long)
@JvmInline value class ProductId(val id: Long)
@JvmInline value class OrderId(val id: Long)

class OrderService {
    fun createOrder(
        customerId: CustomerId,
        productId: ProductId
    ): OrderId {
        // Implementation
        return OrderId(12345)
    }
}

// Usage
val customer = CustomerId(100)
val product = ProductId(200)
val order = OrderId(300)

service.createOrder(product, customer)  // ❌ Compile error - parameters swapped!
service.createOrder(customer, product)  // ✅ Correct

2. Domain Modeling

@JvmInline
value class Money(val cents: Long) {
    val dollars: Double get() = cents / 100.0
    
    operator fun plus(other: Money) = Money(cents + other.cents)
    operator fun minus(other: Money) = Money(cents - other.cents)
    operator fun times(factor: Int) = Money(cents * factor)
    
    fun format(): String = "$%.2f".format(dollars)
}

@JvmInline
value class Percentage(val value: Double) {
    init {
        require(value in 0.0..100.0) { "Percentage must be between 0 and 100" }
    }
    
    val decimal: Double get() = value / 100.0
    
    operator fun times(amount: Money): Money {
        return Money((amount.cents * decimal).toLong())
    }
}

// Usage
val price = Money(9999)  // $99.99
val tax = Percentage(8.5)
val total = price + (tax * price)
println(total.format())  // $108.49

Common Patterns

Use CaseExampleBenefit
IDsUserId, ProductId, OrderIdPrevent ID mix-ups at compile time
UnitsMeters, Kilograms, CelsiusType-safe measurements and conversions
MoneyUSD, EUR, BitcoinPrevent currency mixing errors
TokensApiKey, SessionToken, JwtTokenSecure value handling with type safety
CodesCountryCode, CurrencyCode, PostalCodeDomain-specific validation
CoordinatesLatitude, LongitudePrevent coordinate confusion

Best Practices

DO Use When:

  • Type safety is the primary goal
  • Preventing parameter confusion in APIs
  • Domain modeling requires semantic types
  • You have validation logic for primitive values
  • Working with measurements, IDs, or codes

DON'T Use When:

  • Performance is the only motivation
  • Most usage will be in collections or nullable contexts
  • You need inheritance hierarchies
  • Multiple properties are required

⚠️ Performance Considerations:

// Measure actual impact - boxing is common!
val userIds: List<UserId> = listOf(...)  // All values boxed
val nullableId: UserId? = getUserId()    // Boxed if non-null

// Consider regular classes if mostly boxed
class UserId(val value: Long)  // Always boxed, but simpler mental model

💡 Design Tips:

  1. Focus on type safety first, performance benefits are a bonus
  2. Add validation in init blocks for domain constraints
  3. Provide convenient operators and conversion methods
  4. Use descriptive property names (not just value)
  5. Document boxing scenarios in your codebase

Real-World Example

// Domain model for an e-commerce system
@JvmInline
value class SKU(val code: String) {
    init {
        require(code.matches(Regex("[A-Z]{3}-\\d{6}"))) { 
            "SKU must match pattern XXX-000000" 
        }
    }
}

@JvmInline
value class Quantity(val amount: Int) {
    init {
        require(amount >= 0) { "Quantity cannot be negative" }
    }
    
    operator fun plus(other: Quantity) = Quantity(amount + other.amount)
    operator fun minus(other: Quantity) = Quantity(amount - other.amount)
}

@JvmInline
value class Price(val cents: Long) {
    init {
        require(cents >= 0) { "Price cannot be negative" }
    }
    
    val dollars: Double get() = cents / 100.0
    
    operator fun times(quantity: Quantity): Price {
        return Price(cents * quantity.amount)
    }
    
    fun format(): String = "$%.2f".format(dollars)
}

// Type-safe usage
data class CartItem(
    val sku: SKU,
    val quantity: Quantity,
    val unitPrice: Price
) {
    val totalPrice: Price get() = unitPrice * quantity
}

// This prevents errors like:
// CartItem("ABC-123456", 29.99, 2)  // ❌ Wrong types!
// CartItem(SKU("ABC-123456"), Quantity(2), Price(2999))  // ✅ Correct

Summary

Value classes provide compile-time type safety with potential performance benefits when inlined. Their primary value is preventing type confusion and enabling rich domain modeling rather than guaranteed performance improvements. Use them to make invalid states unrepresentable and APIs impossible to misuse.


Backlinks