In his book Implementing Domain-Driven Design Vaughn Vernon suggests to reference other aggregate roots only by identity and not by reference, like this:
// Aggregate Root
class Order(
val customerId: CustomerId // reference by ID
)
He argues that this would prevent accidental modifications of the referenced aggregate root (since the actual object is not directly available). The other argument is that it is a form of lazy loading.
But referencing only by identity is also cumbersome in many cases since you have to load related objects via their repository in a service.
Accidental modification of a related aggregate root object could also be prevented, if the referencing aggregate root would only hold a read-only view of the referenced aggregate root:
// Aggregate Root
class Order {
val customer: CustomerReadOnly // read-only view reference
}
interface CustomerReadOnly {
val id: CustomerId
val address: Adress
}
// Aggregate Root
class Customer: CustomerReadOnly {
override val id: CustomerId
override var address: Address
private set
fun changeAdress(newAddress: Adress) {
// some business logic ...
address = newAddress
}
}
This would make loading complete objects much more convenient and potentially faster, since an ORM probably need fewer queries. And it would prevent accidental modification, since the read-only view can not be modified.
The lazy-loading aspect could be the same as with ID references, since you can tell the ORM to lazy-load related objects (what is the default in JPA). But, depending on the use case you could also tell your ORM to fetch related objects eagerly, what is more "persistence ignorant" than ID references in my opinion.
A potential downside I see with the read-only view approach is that the borders of an aggregate root (the consistency boundary) might be less obvious. Even with a read-only view it is easily possible to base an invariant on its state, what wouldn't be possible with an ID reference. So with ID references you would immediately see that the aggregate roots might not be properly designed yet.
Another downside might be that the read-only view interface is not really part of the domain language, but since it is conceptually the same as the underlying domain object, I see no major problem here.
Vladimir Khorikov argues that ID references would be a leaky abstraction and should therefore be avoided. However, he misses the point of a clean separation of aggregate root to prevent accidental modification.
Do you see any other downside of read-only views in other aggregate roots or advantages of ID references I might be missing?