Avoid Primitive Obsession in Kotlin with zero-cost abstractions

João Paulo Gomes
WAES
Published in
4 min readOct 26, 2023

This article will explain what primitive obsession is and how to avoid it when programming in Kotlin (backend or frontend). The good news is that you can avoid it using a zero-cost abstraction called Inline Value Class.

How can you avoid primitive obsession when programming in Kotlin

What is primitive obsession?

Primitive obsession is a code smell in software development where primitive data types, such as strings, integers, or booleans, are overused to represent domain concepts instead of creating custom classes or types to encapsulate that domain logic. Example:

fun main() {
val password = "123abc"
println(password)
}

This is a valid operation according to the compiler but invalid according to your domain model. The code doesn't enforce the password business or security strength requirements, so it's possible to represent an invalid state in your code. It also has a security issue because it's possible to log a user password to the console.

So, primitive obsession can let invalid states be represented in your domain and lead to bugs or security issues. A way to solve this issue is to create a class that holds the value and has validation to guarantee a valid state. Example:

data class Password(val value: String) {

init {
require(value.length >= 8) { "Password must be at least 3 characters long" }
require(Regex("[a-z]").containsMatchIn(value)) { "Password must contain at least one lowercase letter" }
require(Regex("[A-Z]").containsMatchIn(value)) { "Password must contain at least one uppercase letter" }
require(Regex("[0-9]").containsMatchIn(value)) { "Password must contain at least one digit" }
require(Regex("[^a-zA-Z0-9]").containsMatchIn(value)) { "Password must contain at least one special character" }
}

override fun toString(): String = "***"
}

This is a nice solution, but it will create some overhead in your application and increase memory usage because now you have an object to hold this state. Kotlin offers a most performant way to achieve the same result.

What are Kotlin Inline Value Classes?

Kotlin Inline Values Classes allow developers to create lightweight, efficient classes that wrap a single value and provide type safety at compile time.

An inline value class:

  • Must have a single property initialized in the primary constructor;
  • Must have the annotation @JvmInlineif it’s going to be compiled to Java bytecodes;
  • Is allowed to declare properties and functions;
  • Can have an init block ;
  • Can have secondary constructors;
  • Cannot have backing fields for properties (only computable fields);
  • Cannot have lateinit;
  • Cannot have delegated properties;
  • Can inherit only from interfaces;
  • Can have a generic type parameter as the underlying type.

Example:

@JvmInline
value class Password(private val value: String) {

init {
require(value.length >= 8) { "Password must be at least 3 characters long" }
require(Regex("[a-z]").containsMatchIn(value)) { "Password must contain at least one lowercase letter" }
require(Regex("[A-Z]").containsMatchIn(value)) { "Password must contain at least one uppercase letter" }
require(Regex("[0-9]").containsMatchIn(value)) { "Password must contain at least one digit" }
require(Regex("[^a-zA-Z0-9]").containsMatchIn(value)) { "Password must contain at least one special character" }
}

override fun toString(): String = "***"
}

Kotlin Zero-Cost Abstraction

From the official documentation:

In generated code, the Kotlin compiler keeps a wrapper for each inline class. Inline class instances can be represented at runtime either as wrappers or as the underlying type. The Kotlin compiler will prefer using underlying types instead of wrappers to produce the most performant and optimized code. However, sometimes it is necessary to keep wrappers around. As a rule of thumb, inline classes are boxed whenever used as another type.

If you declare and always use the declared type, it's guaranteed that the compiler will produce the most performant code. Example:

val password = Password("123ABC@def")

If you use it as generic, declare the type nullable, or use it as an interface, then the wrapper will be used. Example (from the official documentation):

interface Id

@JvmInline
value class UserId(val i: Int) : Id

fun asInline(f: UserId) {}
fun <T> asGeneric(x: T) {}
fun asInterface(id: Id) {}
fun asNullable(i: UserId?) {}

fun <T> id(x: T): T = x

fun main() {
val f = UserId(42)

asInline(f) // uses the underlying type: used as Foo itself
asGeneric(f) // uses the wrapper class: used as generic type T
asInterface(f) // uses the wrapper class: used as type I
asNullable(f) // uses the wrapper class: used as UserId?, which is different from UserId

// below, 'f' first is boxed (while being passed to 'id') and then unboxed (when returned from 'id')
// In the end, 'c' contains unboxed representation (just '42'), as 'f'
val c = id(f)
}

Conclusion

Kotlin offers an elegant solution to avoid primitive obsession. Besides that, it's possible to achieve zero-cost abstraction using it and make your code type safe. So, why are you not using it to represent your domain more expressively?

Do you think you have what it takes to be one of us?

At WAES, we are always looking for the best developers and data engineers to help Dutch companies succeed. If you are interested in becoming a part of our team and moving to The Netherlands, look at our open positions here.

WAES publication

Our content creators constantly create new articles about software development, lifestyle, and WAES. So make sure to follow us on Medium to learn more.

Also, make sure to follow us on our social media:
LinkedInInstagramTwitterYouTube

--

--

João Paulo Gomes
WAES
Writer for

Hi! I’m JP! I work as a Kotlin and Java developer and in my spare time I like to cook. My github https://github.com/johnowl