A few weeks ago Java17 was released as our next LTS-version. In the release we got a few new features and one of them was records. The nature of these records makes them a suitable fit for value objects and will help us reduce the amount of boilerplate code we have in our code bases.
What is a value object?
In Domain-Driven Design, we typically divide our business objects into entities and value objects. An entity is something with an identity and a life cycle while a value object is only relevant for its value. The typical example here is that your wallet is an entity because you can’t just exchange it for another, it needs to be your own. The 100 SEK bill you have in there however, can be exchanged to any other 100 SEK bill. Hence, it is considered a value object.
Why can I just use primitives for my values?
Primitives are good because they are simple. However, it is their simplicity that often gives us problems. First of all, when using primitives we rely quite heavily on the naming of the parameter. Just using an int for quantity makes me ask a few questions; Can a quantity be negative? Am I allowed to change it? Are we talking about the amount of toys to be shipped or boxes?
Best case scenario is that we surround the primitive with all the validation needed for its creation, we push the business logic around and we rely on good naming conventions to make sure that it is used correctly. But even if we can cope with this situation, it is still error prone to send primitives around. I guess we all have encountered methods like send(String email, String text) where we have managed to send in the parameters the wrong way without any help from the compiler.
Value objects solve these issues. First it provides us with a type safety by default, but it also ensures that it is self validated by moving the validation logic into its constructor. Which means that we are coupling all the business invariants with the data making it impossible to create them with data that does not conform to our business rules. e.g for Quantities, we cannot create them with negative numbers.
Value Objects are immutable, which means that they cannot be changed after they have been created. It goes along with the mindset of the 100 SEK bill. We only care about its exact value and we can always exchange it or throw it away or re-create it if needed. Immutability also renders these objects to be perfect to contain business logic. First, we can couple the data and the logic together to achieve high cohesion but it is also forcing us to describe our business in a setting where it is side effect free, deterministic and easy to reason about.
Java example of a Value object
When building a Value Object we want
- Immutability. e.g. private final member fields that cannot change after its creation
- Equality by value and not by identity (remember, we do not care which 100 SEK bill, only that it is a 100 SEK bill)
- A declarative toString
- Getters to get the values
Introducing records
In Java17 (well they were introduced in 14 but part of this LTS) records were introduced. Records can be considered as an immutable class with pre-created equals and hashcode based on value, as well as getters for each member variable. You do not have to create a typical constructor, you just have to define the data you want to store, and everything is generated for you.
Java example of a record
The following code does most of the things in the previous example. When a record is created we get a constructor for the parameters, equality by value, immutability, getter for the parameters and a toString. Hence, no more boilerplate code!
Sounds pretty nice right?
Enforce business invariants
A major advantage of using a value object over using a primitive is that you can enforce business invarians and rules upon creation of the object. It can be as simple as a Quantity enforcing that it cannot be negative. Instead of checking it at every creation, we can let the constructor enforce this for us. Hence, we can ensure that our Quantity always has a valid value.
Records come with a predefined constructor as we saw in the previous example. This one can of course be overridden, but you could also create a no-argument constructor to only execute validation logic.
Perform business logic
Due to its nature of being immutable, we want to push relevant business logic into the value objects. This will make the business logic side effect free, deterministic and easy to test and reason about.
The implementation of business logic has not changed and will look the same in Java11 as in Java17.
What have we learned?
With the introduction of Records we have an easy way of constructing Value Objects. Previously we had to write quite a lot of boilerplate code or had our favourite editor generate it for us. In this example we could go from 43 to 17 lines of code with the same functionality.
Do you want to learn more?
We are always curious about meeting new people to share insights and learn from each other. Read more about Citerus (in Swedish). We look forward to hearing from you!