Value objects are a fundamental concept from domain-driven design that allow us to model values as rich objects. The idea is that representing values as primitives (i.e. string, number, date etc) can mean we are lacking context and meaning in our software. This is sometimes referred to as "primitive obsession". Instead, we can model concepts in our domain as value objects that encapsulate both data and behaviours.
Value objects are immutable objects that are distinguishable only by the state of their properties. They have no unique identifier. If a value object’s properties are the same as those on another value object of the same type they are considered equal. To check if two value objects are considered equal we must compare all of their properties.
The value of value objects
Now we know what value objects are, let’s take a look at some benefits of using them.
Take the concept of money for example. Money could be modelled as a simple number, but what behaviour are we hiding? Can the amount be negative? What about currency? Money is always of a specific currency. Can we add two currencies together? Probably not as different currencies have different values and simply adding doesn't account for exchange rates.
If you have $1 and I have $1, we don’t care or know who has which $1. If we exchange that money, we both end up with the same amount of money. In the real world it makes no difference who has which physical bit of money, and in a computer system there isn’t even any physical money. Money has no unique identity, and so long as the properties of the money are the same we can treat them as equal. A value object may therefore be better suited here than a number.
We could create a value object that has a currency and amount property.
enum Currency {
USD = 'USD',
GBP = 'GBP',
}
type MonetaryAmountProps = {
currency: Currency;
amount: number;
};
class MonetaryAmount {
get currency(): Currency {
return this.props.currency;
}
get amount(): number {
return this.props.amount;
}
constructor(private props: MonetaryAmountProps) {}
equals(other: MonetaryAmount): boolean {
return this.currency === other.currency && this.amount === other.amount;
}
add(other: MonetaryAmount): MonetaryAmount {
if (other.currency !== this.currency) {
throw new Error('Cannot add amounts with different currencies');
}
return new MonetaryAmount({
currency: this.currency,
amount: this.amount + other.amount,
});
}
// more operators like subtract and multiply could exist here...
}
To create an instance of a MonetaryAmount
we can see that a currency and amount are required via the constructor, intrinsically linking the two values. Since currency is defined as an enum we know we can only create a monetary amount with a valid currency.
Next, rather than adding two numbers together that could represent two different currencies, we expose an add()
function on the value object which performs the add operation. This function performs a check to ensure the currencies of the two monetary amounts being added are the same - if not then an error is thrown. The domain behaviour is encapsulated and ensures that business rules are not broken.
Adding two monetary amounts together returns a new instance of MonetaryAmount because value objects are immutable. This immutability is important because it ensures that when two values objects are created equal they will always be equal. The equality of two value objects can be checked in this example via the equals()
function.
To those familiar with it, this may just sound like object oriented programming. In a way, you're right. But the important thing here is we're specifically modelling value objects based on behaviours that are specific to a domain. This is a contrived example but when you have lots of business rules this a great way of encapsulating logic that's easy to understand and reason with.
Want to learn more?
You can try using value objects for yourself by taking an existing primitive value in your codebase and rethinking what domain logic you may be hiding by modelling it as a primitive. You may be surprised how many primitive values actually have more meaning than first meets the eye!
If you enjoyed this post then be sure to check out Discovering Domain-Driven Design, my ebook where I cover more about value objects and other DDD concepts that can help to model business logic and build complex software.