Value Objects: Design heuristics for modelling value objects

Rohit Singh
Javarevisited
Published in
7 min readAug 16, 2022

Prologue

Value Objects are the building blocks of Domain Driven Design and are well defined in Eric Evans DDD. The idea behind here is to dig deeper into the purpose of value objects, define a standard way for modelling them, and look into the solutions that support this design consideration.

Design Considerations

Value objects are defined by their attribute values and are one of the key building blocks in model-driven design architecture. Value objects have no conceptual identity and describe the object for what they are.

Design heuristic #1 : Two value objects are equal if their attributes have same value.

Java is a strongly typed language and so value object equality lies in the attribute values, not their references. One of the key purposes of a value object is that one must be able to share them knowing that its state won’t change after it has been created and passed.

Design heuristic #2 : Value objects should be immutable in nature.

Treat the value object as immutable and do not attach any identity to it. This gives us the freedom to simplify the design and optimize performance. But why does one needs to make it immutable and will it add value to the project and make things easier? We need to understand the purpose here.

Java passes references by value, so you get a copy of the reference, but the referenced object is the same. So, it could happen that callee changed the value of passed reference objects to the methods, and then the caller might get unexpected results.

Consider the below example. Here, we constructed an account object in processSavings() and passed it to checkValid(). In checkValid(), we mistakenly updated the account type, and finally, we have passed the account reference to postAccount() for processing.

But, there is one mistake we did, we updated the account type which was not the intention and thus savings account was wrongly converted to current and processed.

public class Account{
String accountNumber;
String accountType;
public Account(String accountNumber, String accountType){
this.accountNumber =accountNumber;
this.accountType=accountType;
}
public String getAccountNumber(){
return this.accountNumber;
}
public void setAccountNumber(String accountNumber){
this.accountNumber=accountNumber
}
public String getAccountType(){
return this.accountType;
}
public void setAccountType(String accountType){
this.accountType=accountType;
}
}
public class TestImmutability {
public void processSavings(){
Account account = new Account("a0123", "savings");
checkValid(account);
postAccount(account);
/////.....
}
public boolean checkValid(Account account){
// run valid checks
account.setAccountType("current");
return true;
}
}

It might look easy to trace such mistakes here and might not happen, but think if it is a large-scale project with complex value objects which are shared and passed to function across modules, then it would not be easier to locate and fix such bugs.

This is one of the primary reasons one should design value objects as immutable so that once initialized they should not be changed. Immutability also ensures thread safety and the validity of the data in synchronization which favors this consideration.

One might argue that this could cause memory constraints as this might lead to too many copies of the same object in memory that can be shared. But one needs to think of trade-offs that we are making here in terms of performance and secondly if we have effective garbage collection, deletion, and memory availability are a matter of time.

Designing a value object as immutable is a case of following a general rule. If the value of an attribute changes, then we create a different value object, instead of modifying the existing one. But there might be cases where value objects could be designed as mutable.

Design heuristic #3 : Value objects could be mutable, for performance reasons.

If it is mutable, ensure that it is not shared as it will create unexpected results and integrity issues in the model. The factors determining mutable implementations of value objects (quoting Eric Evans’ DDD):

  1. If the value changes frequently.
  2. If object creation or deletion is expensive.
  3. If replacement will disturb clustering.

Designing an Immutable Value Object

Consider the below class defined as an immutable:

public class Account {
private final int id;
private final String type;
private final String currency;
private final LocalDateTime createdDate;
private final List<Entry> entries;
private Account(
int id,
String type,
String currency,
LocalDateTime createdDate,
List<Entry> entries) {
this.id = id;
this.type = type;
this.currency = currency;
this.createdDate = createdDate;
this.entries = entries;
}

public int getId() {
return id;
}
public String getType() {
return type;
}
public String getCurrency() {
return currency;
}
public LocalDateTime getCreatedDate() {
return createdDate;
}
public List<Entry> getEntries() {
return Collections.unmodifiableList(entries);
}

public String toString() {
return "Account{"
+ "id=" + id
+ ", type=" + type
+ ", currency=" + currency
+ ", createdDate=" + createdDate
+ ", entries=" + entries
+ "}";
}

public int hashCode() {
return Objects.hash(id,type,currency,createdDate,entries);
}
public boolean equals(Object another) {
if (this == another) return true;
return another instanceof ImmutableAccount
&& id == another.id
&& type.equals(another.type)
&& currency.equals(another.currency)
&& createdDate.equals(another.createdDate)
&& entries.equals(another.entries);
}
}

This accomplishes our goal, but if we look closely, there’s a lot of boilerplate code here like getter, constructor, toString(), equals(), and hashcode(). In addition to that, we have to repeat the same tedious process for each data class, monotonously creating a new field for each piece of data, creating equals(), hashCode(), and toString() methods; and creating a constructor that accepts each field.

Design heuristic #4 : Use auto code generator for value objects wherever possible to handle boiler plate code.

Lombok, AutoValue, Immutables, and Java Records are some of the widely used tools used to handle such boilerplate code for domain value objects. But which one is the right tooling? It is hard to make a decision if we look at their purpose from the top because one way or another way, they do the same job, but look for fine-grained facts and then make a decision.

Lombok vs AutoValue vs Immutables vs Java Records

Lombok, AutoValue, and Immutables use a standard java annotation processing to handle common code generation for data classes. Let's look at an example. Consider we have a value object Account as below and see how it is represented in each

Lombok:

@Value
@Builder
public class Account {
private int id;
private String type;
private String currency;
private LocalDateTime createdDate;
private List<Entry> entries;
}

AutoValue:

@AutoValue
public abstract class Account {

public abstract int id();
public abstract String type();
public abstract String currency();
public abstract LocalDateTime createdDate();

@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setId(int id);
public abstract Builder setType(String type);
public abstract Builder setCurrency(String currency);
public abstract Builder setCreatedDate(LocalDateTime createdDate);
public abstract Account build();
}
}

Immutables:

@Value.Immutable
@Value.Style(jdkOnly = true)
public interface Account {
int getId();
String getType();
String getCurrency();
LocalDateTime getCreatedDate();
List<Entry> getEntries();
}

Java Record:

public record Account(int id,
String type,
String currency,
LocalDateTime createdDate,
List<Entry> entries) {}

But there are a few subtle differences in how they handle it.

  1. Lombok uses a non-standard approach for code generation, also called hacking the java compiler which makes it fragile and may not be supported in the future with java evolution. Plus, Lombok has a runtime dependency on byte code manipulation libraries like asm. On other hand, AutoValue and Immutables use no such hacks and dependencies, generate code from the defined template, and use the standard java annotation.
  2. AutoValue looks a little verbose whereas Lombok and Immutables look much lean and clean. AutoValue is also strictly immutable means no way of defining mutable objects.
  3. The ordering of fields in the constructor depends on the source in AutoValue whereas in Immutables one can define the order of fields in the constructor.
  4. The account is defined as an interface with just getters in Immutables. By declaring the value object as an interface, we are encapsulating the value object from any side effects and building it as the model revealing clear intention that this is an immutable class and the client will have only access to values. This is the USP of immutables. Lombok and AutoValue do not support it.
  5. Java Records are super clean. Personally, I would always recommend using this over any 3rd party library or tooling. But there are a few limitations to this as of now. Records are purely immutable and transparent data carriers and there is no way you can mutate the data in the record. Lombok and Immutables define ways to declare a class as mutable. Another limitation of Record as of now is that it is under preview mode in Java 14 and will not be available to use until Java 17.

Design heuristic #5 : Design value object as an interface to achieve true immutability.

The complete code comparing Lombok, AutoValue, and Immutables features can be found here: https://github.com/rohsin47/lombok-autovalue-immutables

The below table compares the features of Lombok vs AutoValue vs Immutables on various parameters.

Lombok vs AutoValue vs Immutables

Conclusion

Ideally, value objects should be type safe, null safe, thread-safe, API invisible (encapsulated), have no runtime dependencies, and should have negligible cost to performance.

Lombok and Immutables are good alternatives for creating immutable value objects but it is recommended to use Lombok stable features only such as @Value, @Builder, and @Data for implementation and avoid experimental features. On the other hand, the number of customizations and optimizations that can be achieved with Immutables is impressive.

Features like automatic null handling of attributes, ensuring mandatory attributes, declaring builder, defining a modifier data class as well as JSON serialization support for Jackson bindings make Immutable a better choice for value objects.

References:

--

--

Rohit Singh
Javarevisited

Passionate technology professional, enthusiastic developer, and a devoured reader