3

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?

2
  • 1
    The original notion of aggr. in Eric Evans' Domain Driven Design book does not mandate the use of IDs for referencing other aggregate roots (and is in fact talking about how some aggregates might be reached by traversal). Now, the use of IDs isn't prohibited either - it is a design choice to be made. One issue that comes up is how to delineate aggregate boundaries so that it's clear to developers where they are; as you've noticed, Vernon's approach does have it's own set of pros and cons, but it does make the boundaries clear. But it's definitely not the only "correct" way to do things. Feb 25, 2021 at 20:34
  • I'm not sure how much read only views would help, though - I guess it depends on your usage patterns and the kind of application you're making. They might make working with an aggregate root that only needs to read data from other aggregates easier, but if you need to manipulate another aggregate, you'd still have to somehow obtain a mutable version of it. Feb 25, 2021 at 20:41

2 Answers 2

1

Design is what we do to get more of what we want than we would get by just doing it -- Ruth Malan

Any comparison of two designs is going to be rooted in the "what you want".

class Order {
  val customer: CustomerReadOnly
  // other information that is part of the order aggregate

  fun submit() {
     // ...
  }
}

vs

class Order {
  // other information that is part of the order aggregate
  fun submit(customer: CustomerReadOnly) {
     // ...
  }
}

With the appropriate code, either of these designs can be used to tell the machine to do the right thing. But what else do you want?

One possible answer is that you want a design that clearly distinguishes the object that are part of the Order aggregate from those that are not. The second design is aligned with that distinction: member data is part of the aggregate, method arguments are not. In the first design, the distinction is... fuzzy.


That said, this style of design -- where "domain entities" manipulate their own internal state -- is not the only option. It might instead make sense to model your information inside of anemic data structures, and implement your domain logic within procedures that accept those data structures as an argument.

Your "OR/M entity", then, becomes an anemic data structure that the domain logic acts upon (probably via an interface, to keep the dependency arrows pointing the right direction). That in turn makes it easy to (for example) implement an OR/M entity that implements both the mutable orders interface and the immutable customer interface.

The domain logic really doesn't care whether the customer information is fetched together with the order or separately (which is part of the point of the domain modeling patterns described by Evans -- we shouldn't have to worry about the plumbing when we are implementing our domain logic).

1
  • submit(customer: CustomerReadOnly) would be a good solution, if I wouldn't actually need a collection of related ARs (I tried to keep my example simple). So I'd need to load a collection of related ARs for each object in a list of objects, If I wouldn't use a read-only view. The second approach of separating data and business logic might be a better fit for me needs.
    – deamon
    Feb 27, 2021 at 15:18
1

Why would you create a ReadOnly view of Aggregate Root ? The data will be probably very much like a tree and with nesting. Inconvenient for read.

With regards of Read Only model I would be thinking of some sort of Projection. A flattened model dedicated for read.

1
  • In the actual application I need a collections of related aggregate roots (AR), and not only for a single referencing AR but for many. That makes it very inconvenient to load all the nested objects in a service. But I need all those objects for a complex processing step. If it were only for viewing them, then I'd agree that a specialized projection would be best. But since I need a "real" domain object that is in principle the same as the AR with IDs / read-only view, I cannot come up with a proper solution besides ID references or read-only views.
    – deamon
    Feb 27, 2021 at 15:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.