avatarLuís Soares

Summary

Value objects are immutable, always valid, and carry a value that identifies them, providing logic and domain meaning in software development.

Abstract

Value objects are a crucial concept in software development, particularly in the context of domain-driven design. They are identified by the immutable value they carry and are always valid, meaning they self-validate to prevent invalid instances from existing. Value objects can have logic and portray domain meaning, making them an essential building block in creating a ubiquitous language within a software application. They are used to represent high-level domain concepts, such as mathematical values, identifiers, and abstract types. Value objects are a cure for the primitive obsession anti-pattern and help to reveal intent, making APIs and entities more expressive and easier to understand.

Bullet points

  • Value objects are identified by the immutable value they carry.
  • They are always valid, meaning they self-validate to prevent invalid instances from existing.
  • Value objects can have logic and portray domain meaning.
  • They represent high-level domain concepts, such as mathematical values and identifiers.
  • Value objects are a cure for the primitive obsession anti-pattern.
  • They help to reveal intent, making APIs and entities more expressive and easier to understand.
  • Value objects are an essential building block in creating a ubiquitous language within a software application.

The Value of Value Objects 💊

Value objects are identified by the immutable value they carry, are always valid, can have logic, and portray domain meaning. Let’s explore those characteristics.

Carry a value

Value objects represent high-level (domain) concepts. They can be categorized as:

  • Mathematical values: represent coordinates on a continuous scale. e.g. Percentage, Money, Temperature, Point, Color, Date, Date Range, and Distance.
  • Identifiers: discrete values used to identify some entity. e.g. Email, Locale, Currency, Telephone number, IP address, URL, File path, and Product identifier (enums behave as identifiers but they’re limited to a hardcoded set of values).

Usually, a value object holds a single primitive value [🚀], but it can hold more. For example, an RGBA color holds 4 integers; a 3D point holds three numbers.

// The literal value is kept inside the value object
data class Email(val value: String)

fun main() {
    val newEmail = Email("[email protected]")
    assert("[email protected]" == newEmail.value)
}

Value objects don’t make sense on their own. They exist in the context of an entity. You might create a new one to run a findByEmail for example, but other than that, they exist only to serve as entity data.

Same value? Always equal

When you take a pill from a bottle, it doesn’t matter which one you pick. A value object's state matches its identity. Two value objects carrying the same value are identical; both represent the same concept. When compared, they are always equal. Email(“[email protected]”) will always equal Email(“[email protected]”) regardless of their references [🚀]:

// Value objects with the same value are always equal
// (in some languages you may have to define toEquals)
data class Email(val email: String)

fun main() {
  assert(Email("[email protected]").equals(Email("[email protected]")))
  assert(Email("[email protected]") == Email("[email protected]"))
}

Objects that are equal due to the value of their properties […] are called value objects. Value Object (Martin Fowler)

For most purposes, value objects can be seen and treated as primitive values because they behave the same way. They’re all value types in opposition to reference types (you pass by value rather than by reference).

Immutable

While an entity has a long-lived identity granted by its properties, a value object’s identity is solely given by the piece of data it carries. Since it does not evolve, it means it does not have a life of its own. Immutability is what makes value objects what they are.

Immutability is an essential property in functional programming but it should be pursued in objected-oriented as well. It pays to make the objects immutable, namely to prevent the aliasing bug and guarantee thread safety. For example, once an Email is created, you can’t change its value [🚀]:

// Compilation error when reassigning the value of a value object
data class Email(val value: String)

fun main() {
  Email("[email protected]").value = "[email protected]" // 🚨 Val cannot be reassigned
}

Immutable values can freely be passed around. There is no need to create copies. Even concurrent access is safe without modifications. Testing is simplified to creating different instances via the constructor and then asserting the returned results of properties and methods. Immutable objects also encourage side-effect free functions and those come with their own set of advantages. Kotlin and Domain-Driven Design — Value Objects

Abstract a type

Value objects are the cure to an anti-pattern called primitive obsession — using primitive values (i.e. naked primitives) everywhere.

Value objects enclose their real type as an implementation detail. For example, you might be changing the representation of a price to include the currency, or you may be changing the identifier of the User entity from integer to string — you wouldn’t have to change entities that use it or declarations like this:

findById(userId: UserIdentifier)  // all the calls are kept the same

By using a value object we can ensure that clients won’t rely on raw data, not on the particular primitive type of that data. Advanced Web Application Architecture

For functions with long lists of parameters, most of them the same type, value objects can be of great help, in terms of documentation and error-checking.

Intent revealing

With a value object, you create a new semantic level that lets you express much more clearly and closer to the domain of the app.

Being abstract is something profoundly different from being vague (…) The purpose of abstraction is not to be vague but to create a new semantic level in which one can be absolutely precise. Edsger W. Dijkstra

Value objects are domain-specific types that enrich your type system, promoting domain terminology. They’re one of the building blocks of domain-driven design, playing an essential role in the ubiquitous language. Primitive types don’t convey any meaningful domain knowledge.

They make the domain explicit, e.g. by using Money as a wrapper instead of just two fields of type BigDecimal and String […]. Readers of the code will understand it’s about money and maintainers will be unable to pass something not representing money. In addition, the wrapper can contain validations and thus additional domain knowledge and also increase security. Kotlin and Domain-Driven Design — Value Objects

The self-validation and logic that value objects have within them are also a source of contextually placed know-how about the domain.

Value objects help APIs and entities reveal their intent, acting as code self-documentation.

In entities

Consider the Customer entity where most of its properties can be value objects [🚀]:

import java.util.Date

// Customer entity without value objects
data class CustomerOld(
    val id: Int,
    val personalEmail: String,
    val salutation: String,
    val firstName: String,
    val lastName: String,
    val language: String,
    val createdAt: Long
)

🆚

// Customer entity with value objects
data class Email(val email: String)
enum class Salutation { MR, MS }
data class Name(val value: String) {
    val first get() = value.split(" ").first()
    val last get() = value.split(" ").last()
}
data class Locale(val value: String)

data class Customer(
    val id: Customer.Id,
    val personalEmail: Email,
    val salutation: Salutation,
    val name: Name,
    val language: Locale,
    val createdAt: Date,
) {
    data class Id(val value: Int)
}

They’re even more important in the functional programming paradigm because you’re mostly passing lambdas around. It also reveals intent when resorting to generics:

val articleMappings: Map<String, String>
🆚
val articleMappings: Map<ArticleCategory, Region>

In APIs

Compare these signatures:

fun notifyClient(recipient: String)
🆚
fun notifyClient(recipient: Email)

The parameter of the first signature doesn’t tell it’s expecting an email, unlike the second signature.

IDEs can help with names and parameter types

Compare these example invocations — the second doesn’t allow any mistake regarding the order of the arguments:

sendSms("test123", "text")
🆚
sendSms(UserId("test123"), "text")

Always valid

Do you keep validating product identifiers everywhere? How do you know that you have valid telephones floating around your application? Can you ensure no corrupt data is stored in the database? Scattered validators pollute all the code. They might be inconsistent and you may forget to call them.

A good way to detect knowledge duplication is to ask what happens i we want to change something. What effort is required? How many places will we need to look at and change? Understanding the Four Rules of Simple Design

Value objects self-validate to prevent invalid value objects from existing in the first place (the problem of failure atomicity is solved by design). The most typical example is the Email value object: rather than having multiple controllers checking if a string is a valid email, you can have an Email value object in the domain; then, your entities and use cases make use of it [🚀]:

// Self-validated value object (notice the similarity with Kotlin's toInt/toIntOrNull)
data class Email(val email: String) {
    init {
        require("^\\S+@\\S+$".toRegex().matches(email))
    }
}

fun String.toEmail() = Email(this)
fun String.toEmailOrNull() = runCatching { Email(this) }.getOrNull()

fun main() {
    val result = runCatching { "invalid.coldmail".toEmail() }
    assert(result.exceptionOrNull() is Exception)
    assert("invalid.coldmail".toEmailOrNull() == null)
    assert("[email protected]".toEmailOrNull() is Email)
}

This ensures you don’t have invalid emails floating around; it is also a good type of DRY. This can be applied to any primitive: the Quantity value object prevents negative quantities; Name prevents empty strings; Angle ensures values between 0 and 360. Self-validation is the only way to ensure invalid value objects don't exist at all. These validations are known as domain invariants: something always true based on the domain knowledge. It’s worth creating a value object if there’s at least one domain invariant you want to ensure.

By using and validating value objects, one does not only catch errors early on but also improves security. It’s also valuable information about the domain. Kotlin and Domain-Driven Design — Value Objects

📝 Value objects and entities should be self-validated.

Value objects help to enforce proper types, namely by editors, type checkers, and compilers. Using them in all entities and APIs ensures the domain constraints are always followed.

Can have logic

As with self-validation, you don’t want to repeat value objects’ related logic everywhere they’re used. Instead, you should encapsulate that logic in the value object itself. This is true for simple getters [🚀]:

// Adding logic to the email data class - getter
fun main() {
  assert("best.com" == Email("[email protected]").host)
}

data class Email(val email: String) {
  val host = email.substringAfter("@")
}

It’s also true if you want to create new methods. Let’s define the plus of two points [🚀]:

// Adding methods to a value object (Kotlin operator overloading)
fun main() {
    assert(
        Point(-3, 9) == Point(1, 2) + Point(-4, 7)
    )
}

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point) = copy(x + other.x, y + other.y)
}

Bonus: self-normalized

For flexibility reasons, and to avoid weird bugs, you might want to be more open regarding the input that you’re willing to receive. For example, you could automatically convert " [email protected] " to "[email protected]" or "john doe" to "John Doe".

⚠️ Normalization is part of parsing, which is an I/O responsibility. However, value objects belong to the domain layer, in clean architecture. This is why self-normalization is an optional feature. While you may not self-normalize value objects, make sure you self-validate them. Otherwise, you may end up with a database full of repeated emails with different letter casings.

Conclusion

A value object is an envelope carrying simple immutable data. It’s solely identified by that data whereas an entity has a regular identifier with multiple pieces of data. Both can have associated logic.

A primitive obsession in your app is almost as bad as a user form with text fields for everything or a database using strings for everything: you lose meaning and allow wrong input. Value objects bring semantics to your APIs and entities. In the context of an app, I see value objects as a replacement for primitive types. In some cases, they’ll make the compiler help you to reduce mistakes.

Given the low effort involved and its great benefits, I recommend recognizing the possible value objects and creating them early in the project. You’ll have a warning sign the moment you do any kind of logic with literal values (e.g. parsing, splitting, validation, conversion, operations).

Value objects are valuable. Most of the languages already have native support for them (e.g. data classes in Kotlin, Ruby, Python, records in Java and C#).

Further reading

Domain Driven Design
Value Objects
Clean Architecture
Programming
Kotlin
Recommended from ReadMedium