The Problem: Primitive Obsession
You’ve probably seen this pattern in production code: database IDs represented as Long, user emails as String, currency amounts as Double. On the surface, it’s simple and pragmatic. But it introduces a subtle bug vector—primitive obsession.

Imagine you have a function that accepts a user ID and a post ID, both as Long:
fun deletePost(userId: Long, postId: Long) {
if (userIsAdmin(userId)) {
db.delete(postId)
}
}
The compiler won’t stop you from accidentally swapping the arguments. Or passing a currency amount where an ID is expected. Or confusing a timestamp in milliseconds with seconds. These mistakes are silent, expensive, and hard to catch.
Kotlin’s @JvmInline value class solves this by giving you type safety with zero runtime overhead—no wrapper objects, no allocation, no performance penalty.
What Is a Value Class?
A value class is a lightweight wrapper around a single primitive value. The compiler inlines the wrapper at call sites, so at runtime, the value class doesn’t exist—it’s just the underlying primitive.
@JvmInline
value class UserId(val id: Long)
@JvmInline
value class PostId(val id: Long)
fun deletePost(userId: UserId, postId: PostId) {
if (userIsAdmin(userId)) {
db.delete(postId)
}
}
Now the compiler prevents the mistake entirely. You can’t pass a PostId where a UserId is expected. You can’t accidentally swap them. And because the value class is inlined, there’s zero memory overhead—the function receives the raw Long values directly, just like before, except with compile-time safety enforcing you passed the right one.
Zero Allocation: How Inlining Works
Here’s where value classes shine for performance. Let’s look at the compiled bytecode:
// Kotlin source
@JvmInline
value class Email(val address: String)
fun validateEmail(email: Email) {
println(email.address)
}
fun main() {
validateEmail(Email("user@example.com"))
}
The Kotlin compiler doesn’t create a wrapper object. Instead, it transforms the call to:
// Compiled Java bytecode (conceptually)
void validateEmail(String address) {
System.out.println(address);
}
void main() {
validateEmail("user@example.com");
}
The value class disappeared. No allocation, no garbage collection pressure, no object overhead. You get the type safety of a wrapper class with the performance of a primitive. This is why @JvmInline is so powerful for Android—every allocation you avoid helps your app run smoother.
Real-World Android Examples
Value classes shine in several Android scenarios. Here’s a practical navigation example:
@JvmInline
value class ArticleId(val id: Long)
@JvmInline
value class AuthorId(val id: Long)
// Type-safe navigation
fun navigateToArticle(articleId: ArticleId) {
val bundle = bundleOf("article_id" to articleId.id)
findNavController().navigate(R.id.action_to_article, bundle)
}
// Can't accidentally swap IDs
navigateToArticle(articleId = ArticleId(123)) // OK
// navigateToArticle(authorId) // Compiler error!
For database operations, value classes eliminate the risk of passing IDs in the wrong column:
@JvmInline
value class CustomerId(val value: Long)
@JvmInline
value class OrderId(val value: Long)
@Dao
interface OrderDao {
@Query("SELECT * FROM orders WHERE customer_id = :customerId")
fun getCustomerOrders(customerId: CustomerId): List
@Delete
fun deleteOrder(orderId: OrderId): Int
}
Room has added support for value classes with KSP, though you may still need a TypeConverter in some query scenarios. The compiler inlines the wrapper at call sites, but Room’s annotation processor needs to understand the mapping. With recent Room versions and KSP, this works out of the box for most cases — and you still get compile-time type safety preventing you from confusing customer and order IDs.
Financial and Measurement Calculations
Value classes are particularly useful for domain-specific types in business logic:
@JvmInline
value class Price(val cents: Long)
@JvmInline
value class Discount(val percent: Int)
@JvmInline
value class Tax(val percent: Double)
fun calculateFinalPrice(
price: Price,
discount: Discount,
tax: Tax
): Price {
val discounted = price.cents * (100 - discount.percent) / 100
val withTax = (discounted * (100 + tax.percent) / 100).toLong()
return Price(withTax)
}
This prevents costly mistakes where you accidentally pass percentages where cents are expected. Each type clearly documents its intended unit of measurement.
Important Limitations of Value Classes
While powerful, value classes have constraints:
- Single property: A value class must wrap exactly one property. No secondary fields allowed.
- No identity: You can’t use reference equality (===) on value classes—they compare by value, not by object identity.
- No inheritance: Value classes can’t extend other classes (though they can implement interfaces).
- Constructor visibility: Since Kotlin 1.8.20, value classes support private constructors — useful for enforcing validation via a companion factory method.
- Platform limitations: When called from Java, value classes are unboxed, so Java callers see the underlying type.
These aren’t bugs—they’re intentional constraints that enable the inlining optimization.
Combining Value Classes with Builders
For complex types, pair value classes with Kotlin’s collection builders like buildList and buildMap to maintain type safety across your domain model:
@JvmInline
value class TagId(val id: Long)
@JvmInline
value class ArticleId(val id: Long)
data class Article(
val id: ArticleId,
val title: String,
val tags: List<TagId>
)
fun createArticle(title: String, tagIds: List<Long>): Article {
val tags = buildList {
tagIds.forEach { id -> add(TagId(id)) }
}
return Article(ArticleId(generateId()), title, tags)
}
This ensures that even within collections, your IDs are type-checked. You can’t accidentally pass a list of Long where a list of TagId is expected.
Measuring Performance Impact
If you’re curious about the actual runtime performance of value classes, check out our guide to measuring execution time with measureTimedValue and Duration API. You’ll see that value classes have negligible overhead compared to raw primitives, while providing enormous safety gains.
Best Practices for Value Classes
- Use them for domain types: IDs, emails, URLs, prices, quantities—anything that wraps a single value with semantic meaning.
- Make them inline: Always use @JvmInline to ensure zero runtime overhead.
- Document the unit: If wrapping a numeric type, clearly state the unit in documentation or class name (e.g., PriceInCents, DurationInMs).
- Validate in the property initializer: Use a secondary constructor for validation if needed, but remember the value class itself is inlined.
- Keep them simple: Don’t overcomplicate value classes with too much logic—they’re type-safety wrappers, not full domain objects.
The Broader Philosophy
Value classes embody a core Kotlin principle: correctness without compromise. You get type safety as strong as a full wrapper class—preventing entire categories of bugs at compile time—but with the performance of a primitive. That’s why they’re essential for building robust, fast Android apps.
Compared to traditional wrapper classes, value classes are a no-brainer. There’s literally no reason not to use them for domain types in your business logic. The compiler does the heavy lifting; you just get the safety benefits.
This post was written by a human with the help of Claude, an AI assistant by Anthropic.
