skip to content

Why would you wrap a String or Long in a value class such as UserId or Email instead of using the raw primitive type directly?

level: juniorimportance: must knowfreq 70%

answer

  1. Stringly-typed -> distinct compile-time type
  2. UserId vs OrderId can't be swapped
  3. value class + @JvmInline wraps ONE primitive
  4. Type safety with ~zero overhead via inlining
  5. Home for validation + domain methods

basics

~20 s

To make the type meaningful and safe. A plain String could be anything; a UserId can only be a user id, so the compiler stops you from mixing up a user id with an order id or an email.

solid answer

~40 s

Raw primitives are 'stringly typed': a function taking two String parameters (userId, email) lets callers swap them with no compile error. Wrapping each in a separate value class (`@JvmInline value class UserId(val raw: String)`, `value class Email(val value: String)`) gives each a distinct compile-time type, so the compiler rejects passing an Email where a UserId is expected. You get domain-level type safety and self-documenting signatures with essentially zero runtime overhead, because the compiler inlines the wrapper to the underlying primitive in most cases. This is the canonical use case for value classes: typed identifiers and typed scalars (Email, Money, Percentage). It also gives a natural home for validation and domain methods on that concept.

code

kotlin · 9 lines
kotlin
@JvmInline value class UserId(val value: String)
@JvmInline value class Email(val value: String)

fun sendWelcome(id: UserId, to: Email) { /* ... */ }

val id = UserId("u-123")
val email = Email("[email protected]")
sendWelcome(id, email)        // OK
// sendWelcome(email, id)     // compile error: types swapped

go deeper

for a junior

Knows the motivation: a value class makes a raw primitive a distinct, meaningful type so the compiler catches mix-ups.

for a middle

Contrasts it with type alias and regular class, and notes the inlining gives near-zero overhead.

for a senior

Frames it as eliminating primitive obsession at the domain boundary and adding validation, while understanding the one-property constraint.

for a principal

Discusses where typed primitives belong in the architecture (domain model vs DTO edges), serialization implications, and team conventions for adopting them.

## The problem: primitive obsession When a domain concept (a user id, an email address, a quantity of money) is represented by a raw primitive like `String`, `Long`, or `Int`, the type system can't tell two concepts apart. Both `userId` and `orderId` are just `String`, so the compiler happily lets you swap them: ```kotlin fun transfer(fromAccount: String, toAccount: String) { /* ... */ } // Caller accidentally swaps the arguments — compiles fine, breaks at runtime transfer(toAccount, fromAccount) ``` This is called **primitive obsession** or being 'stringly typed'. ## The fix: a value class as a typed primitive A **value class** (declared `value class` and, on the JVM, annotated `@JvmInline`) wraps **exactly one** read-only property. It introduces a brand-new compile-time type around an existing primitive: ```kotlin @JvmInline value class AccountId(val value: String) fun transfer(from: AccountId, to: AccountId) { /* ... */ } ``` Now `AccountId` and, say, `Email` are different types — the compiler rejects passing one for the other. You make illegal states unrepresentable at compile time. ## Why not just a regular class? A regular `class Email(val value: String)` also gives type safety, but it allocates a real object on the heap. A **value class** is special: the Kotlin compiler tries to **inline** it — at runtime it is represented directly by its single underlying value (the `String`), with no wrapper object allocated in the common case. So you get the safety of a distinct type with (usually) the performance of the raw primitive. (Boxing exceptions exist but are out of scope here.) ## Bonus benefits - **Self-documenting APIs:** `fun findUser(id: UserId)` reads better than `fun findUser(id: String)`. - **A home for behavior/validation:** you can add an `init` block (Kotlin 1.4.30+) to validate, and add methods/computed properties on the concept. - **Refactor safety:** changing the underlying representation later is localized. ## Keywords to know `value class`, `@JvmInline`, single `val` property, primitive obsession, type safety, inlining.

  • Does wrapping a String in a value class cost a heap allocation on every call?
    Usually no — the compiler inlines the wrapper so it is represented by the underlying String directly. An object is only materialized in specific boxing situations (nullable use, generics, etc.), which is a separate topic.
  • Could you achieve the same type safety with a type alias?
    No. A `typealias Email = String` is just a different name for the same type, so Email and String stay interchangeable and the compiler offers no protection. A value class creates a genuinely distinct type.

Like labeling identical jars 'Salt' and 'Sugar' — same white powder underneath, but the labels stop you from grabbing the wrong one.

saying these in an interview costs you the question

  • Claiming a value class can wrap multiple properties for an id
  • Saying type alias gives the same compile-time safety
  • Thinking every value-class usage allocates a heap object
  • Confusing value class with a regular data class wrapper
  • Believing primitives already give domain type safety

context