skip to content

Use Cases & Limits

Typed primitives like UserId and Email are the canonical use: they stop you passing an order number where a customer number belongs. The limits — exactly one property, no inheritance, no backing-field properties — are what you weigh against a plain data class.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

What are the structural constraints on a value class — how many properties can it have, and can it have ordinary properties with backing fields?

level: middleimportance: must knowfreq 60%

basics

~10 s

A value class must have exactly one value (one constructor property). It can't have extra stored properties — only computed ones. That single value is what it really is underneath.

open as a page

Can a value class participate in inheritance — extend another class, be extended, or implement an interface? Why or why not?

level: middleimportance: should knowfreq 45%

basics

~10 s

A value class can't extend another class and can't be a parent of others — it's effectively final with no superclass. It can still implement interfaces.

open as a page

How do you enforce that an Email or Percentage value class only ever holds a valid value, and what are the limits of doing so?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Put a check in the value class's init block so construction fails for bad input. But it can't guarantee validity if the value is created by deserialization or unsafe casts that bypass the constructor.

open as a page

You need a Money type and a Coordinates(lat, lng) type. For each, decide between a value class and a data class and justify the choice.

level: seniorimportance: should knowfreq 50%

basics

~10 s

Money is one value, so a value class fits and stays cheap. Coordinates has two fields, which a value class can't hold, so use a data class.

open as a page