Give primitive values domain identity
A value class wraps one value while allowing the compiler to avoid wrapper allocation in some contexts. On JVM, declare it with @JvmInline. Boxing can still occur through nullable, interface, or generic boundaries.
@JvmInline
value class LessonId(val value: String) {
init { require(value.isNotBlank()) }
}
fun lessonUrl(id: LessonId): String = "/lesson/${id.value}"
fun main() { check(lessonUrl(LessonId("kotlin")) == "/lesson/kotlin") }
This example validates nonblank IDs but does not make arbitrary input URL-safe; production routing needs a stricter identifier policy or encoding. The wrapper prevents accidentally passing any unrelated string to lessonUrl.
Use value classes for semantic type safety first. Do not promise allocation-free execution without examining generated code and the actual call context. Java interoperability and name mangling also require care in published APIs.
Exercise
Add a distinct LearnerId and confirm it cannot be passed to lessonUrl. Compare behavior when storing IDs in a generic list.