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()
andhashCode()
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 Case | Example | Benefit |
---|---|---|
IDs | UserId , ProductId , OrderId | Prevent ID mix-ups at compile time |
Units | Meters , Kilograms , Celsius | Type-safe measurements and conversions |
Money | USD , EUR , Bitcoin | Prevent currency mixing errors |
Tokens | ApiKey , SessionToken , JwtToken | Secure value handling with type safety |
Codes | CountryCode , CurrencyCode , PostalCode | Domain-specific validation |
Coordinates | Latitude , Longitude | Prevent 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:
- Focus on type safety first, performance benefits are a bonus
- Add validation in
init
blocks for domain constraints - Provide convenient operators and conversion methods
- Use descriptive property names (not just
value
) - 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