6 min read

How I use the new Inline Value Classes in Kotlin

Learn to apply the concept of Value Objects from Domain-Driven Design in Kotlin by using value classes

Since Kotlin 1.5, the long time awaited inline value classes have finally become stable. They were first introduced in Kotlin 1.2.30 as an experimental feature.

Some of Kotlin’s classes are already very popular like data class and sealed class. The new value class comes to solve a different problem.

What are inline classes?

Kotlin has a variety of class types: class, data class, sealed class, enum class and the “new” value class. Value classes are value-based classes that wrap primitive values. As of now, Kotlin value classes only support wrapping one single primitive value.

Value classes provide type safety. However, wrapping a primitive type can be very costly in terms of performance.

These type of classes are called inline classes because the compiler will “unbox” them to directly use the underlying type for performance improvement.

How are they different from type aliases?

Type aliases introduce an “alias” for an existing type. This means that a Name type alias is just another way to call the type String. Therefore, we could assign a String value to a Name type alias.

However, value classes introduce a new type and cannot be assigned. A Name type will not be the same type as a String.

The Kotlin documentation has a nice example.


Domain-Driven Design

First, let us rewind and talk about Domain-Driven Design and its role in order to have a better understanding of the “value” (pun intended 😜) that value classes bring.

Domain-Driven Design A.K.A. DDD is a software development approach that was first introduced by Eric Evans on his well known book Domain-Driven Design: Tackling Complexity in the Heart of Software.

This approach claims that the software’s structure and the language it uses should be deeply connected to the business domain. For example, if a software manages a cargo fleet of vessels, there will be concepts like “voyage”, “leg”, “vessel”, “load” or “distance” represented by classes and methods.

In DDD, there are multiple concepts to represent domain models. This time we will talk about Value objects. Let us see their main features.

No identity. Value equality

Value objects do not have an identity. Therefore, two objects with the same set of property values are considered equal.

For example, if we have two books with the same title, author, editor, etc., they are the same book and we cannot distinguish them. Within our software system they will be value objects.

They are immutable

Once a value object is created, it cannot change. The only way to change its value is by creating a new object. Therefore, classes representing value objects must not have setters.

Validation

The fact that a value object exists should be proof that it contains valid data. This means that before creating an instance, a value object must verify its validity.

For example, when creating a value object representing an e-mail address, if the e-mail address is not valid, the value object will not be created and an error may be thrown.


What are the benefits of using value objects?

Type safety

A common code smell is Primitive Obsession, where primitive data types are used to represent domain models. For example, we use a Long to represent a timestamp or a String to represent an e-mail.

Because of this, a common flaw is that, there may be multiple properties of the same type:

class Person(
  val firstName: String,
  val lastName: String,
  val age: Int,
  val siblings: Int,
  ...
)

or we can make mistaked by mixing up arguments on a method:

fun setPersonData(firstName: String, lastName: String, age: Int, siblings: Int)...

setPersonData("Smith", "John", 0, 24)

In Kotlin we could use named parameters to prevent easy mistakes, but mistakes can still happen. How would it look if we use value objects?

class Person(
  val firstName: FirstName,
  val lastName: LastName,
  val age: Age,
  val siblings: Siblings,
  ...
)

and method calls cannot be mixed up anymore:

fun setPersonData(firstName: FirstName, lastName: LastName, age: Age, siblings: Siblings)...

setPersonData("Smith", "John", 0, 24) // This is not be possible anymore

// instead it will be:
setPersonData(
  FirstName("John"),
  LastName("Smith"),
  Age(24),
  Siblings(0),
)

This types make our code easier to read too!

Validation

As mentioned before:

The fact that a value object exists should be proof that it contains valid data.

Therefore, an Age object will never contain a negative value and a FirstName or LastName will never be empty or blank.

This brings tremendous benefits to our codebase as we only need to check validity of data upon object creation and invalid objects are simply impossible to exist (unless we have a bug 🤷‍♂️).

Flexibility

Another big benefit that is not so obvious is the fact that value objects provide us flexibility.

Imagine we have a userId represented as an integer. If we were to change the type from an Int in Kotlin to a String (to use UUID, for instance), we would need to change it everywhere it is used, having great impact in our codebase.

Using a value object, a userId of type UserId will always be the same, regardless of the underlying primitive type. We could switch from an integer to a string with minimum impact to our codebase.

Readability

At this point, it goes without saying that readability improves as:

fun sendMessage(email: Email, subject: Subject, message: Message)

is easier to read than:

fun sendMessage(email: String, subject: String, message: String)

What about the drawbacks?

Obviously, creating a value object for every single primitive can easily bloat our codebase. Therefore, it is probably smart to use them at the most critical places.

In Kotlin and other programming languages for instance, serialization of non-primitive types comes with some overhead. We usually have to provide a TypeAdapter so that our serialization tool knows how to handle our own types.

In this case, it is more pragmatic to convert our value objects to their primitive counterparts in the Data Trasfer Objects (DTOs). For example, a userId will be of type UserId across our whole domain, but when declaring a UserDto to send to an API or to store in a database, we could declare it as a String.


How can I start using value classes?

To declare a value class we can do it like this:

value class UserId(val value: String)

If we are using Kotlin in the JVM, we have to use the @JvmInline annotation before the class declaration.

@JvmInline
value class UserId(val value: String)

The reason why we need to use the @JvmInline annotation is that Kotlin wants to provide value classes as a feature early on and by adding this annotation it will compile to primitive JVM Value Types whenever Project Valhalla is released.

What can we use them for?

Since value classes support some functionality from regular classes, we can take advantage of this to enforce them as value objects from the DDD perspective.

In the init block we can validate the value so that the object is never created with invalid data. As an example:

@JvmInline
value class Age(
    val value: Int,
) {
    init {
        require(value >= 0) { "age must be >= 0: '$value'" }
    }
}

In this case, the Age object will always represent a positive number. Trying to create an object with a negative value will throw an IllegalArgumentException.

Additionally, we can have meaningful error messages that directly point to the cause of error. As we can see, we are passing the error message to use in the lambda parameter of the require method.

Another example where we can have extra validation is when representing a country code:

@JvmInline
value class CountryCode(
    val value: String,
) {
    init {
        require(value.length == 2) { "Country code must be 2 characters" }
        val validCountryCodeValue = Locale.getISOCountries()
            .firstOrNull { it.equals(value, ignoreCase = true) }
        requireNotNull(validCountryCodeValue) { "Invalid country code: '$value'" }
    }
}

In this case we make sure that the code has only 2 characters and that it is a real country code. Because we have two require verifications, we can write two different messages which will point to the specific issue.

The extra mile

Kotlin is great programming language. In my opinion it offers a very developer-friendly API.

Like many other languages, Kotlin allows us to overload operators. One of this operators is the less known invoke operator. Objects with an invoke method can be called as a function.

With this, there is a way to create a factory method using the invoke operator and make it look like a normal constructor for the caller.

Because the companion object is a special object in Kotlin, we can overload its invoke operator.

How can we use it in our value class to make them more powerful and go the extra mile?

@JvmInline
value class CountryCode private constructor(
    val value: String,
) {
    init {
        require(value.length == 2) { "Country code must be 2 characters" }
        val validCountryCodeValue = Locale.getISOCountries()
            .firstOrNull { it == value }
        requireNotNull(validCountryCodeValue) { "Invalid country code: '$value'" }
    }

    companion object {
        operator fun invoke(value: String?): CountryCode {
            requireNotNull(value) { "Country code cannot be null" }
            return CountryCode(value.uppercase())
        }
    }
}

In this example, we want to expose a factory method that accepts a nullable type and checks if it is null.

Why do we want to do this? Because we want to just call the factory method with whatever value we are provided without a need for null checks or ? in the call chain. Also because we want to isolate the logic that knows what to do if we want to create this class with a null value.

In the example we also modify the value before we create an instance of the CountryCode value class. Modifying val values during object creation is not possible when calling regular constructors.

In this case we want our underlying value to contain only uppercase letters.

We can also notice that the primary constructor is now private.

How would it look now when we create an instance of CountryCode?

val countryCode = CountryCode("es")

It looks the same!

This is because Kotlin allows us to omit the name of the companion object and invoke method. This is a list of calls to our factory method; they are all equivalent:

// Method 1
CountryCode.Companion.invoke("es")

// Method 2
CountryCode.Companion("es")

// Method 3
CountryCode("es")

What do we have now?

From now on in our codebase, whenever we have an instance of CountryCode, we know that:

  • It is valid
  • It is a 2-chars country code
  • It is uppercase

Summary

Value classes are a very nice and practical addition to our Kotlin toolbox. They provide stronger type safety, validation capabilities, readability and flexibility.

Person chuckling

Thank you JetBrains for the Value Classes in Kotlin!

We have also seen how we can provide a factory method using the invoke operator and go the extra mile.

Like every tool, we must use it for the right problem. So we should use them carefully, otherwise we risk bloating our codebase.

Personally, I like to use them whenever I have a type that I want to validate and to remove ambiguity and prevent mistakes.

Thanks for reading!

References

If you found this article interesting, share it!


Follow my journey

Get the latest updates in your inbox 📨

    No spam. Unsubscribe as your heart desires.