Search code examples
domain-driven-design

What is the intuition behind using value objects in DDD when storing the references (ids) to other Aggregates


Let me preface this by apologising if this was asked before (I could not find a similar question when doing a quick search).

The DDD pattern is centered around defining and isolating aggregates to split the business complexity into a more manageable chunks, and it is strictly forbidden for an aggregate to hold any kind of relation to other aggregates. The references to the other aggregates are instead to be stored as ids that can then be used to fetch the other aggregates on demand. Therefore a single aggregate would only contain its properties, value objects and references to other aggregates.

To use a common example (User, Order) it would look as such:

public class User {
  private Long id;
  private List<Long> orders;
}

and

public class Order {
  private Long id;
  private Long userId;
}

However I have seen multiple sources use another layer of encapsulation for the aggregate references, turning them from the property types (as defined in the example above) into value objects like shown below:

public class User {
  private Long id;
  private List<OrderId> orders;
}

and

public class Order {
  private Long id;
  private UserId userId;
}

I am rather new to DDD so I want to understand the benefit of doing so when working with non-composite ids. On the first glance I see a lot of pretty obvious drawbacks (or such they seem to me!), like explosion in quantity of common base types, serialization issues, extra complexity when working with the code and accessing the values stored within these holders, however I am sure that it would not be done so without a very good reason that I am overlooking somewhere.

Any comments, thoughts or feedback would be very welcome!


Solution

  • This is a domain abstraction

    private UserId userId;
    

    This is a data structure

    private Long userId;
    

    Your domain logic (probably) doesn't care, or need to care, about the underlying data structure that supports representations of UserId, or how they are stored in the database, or any of that nonsense.

    The broad term is "information hiding" -- creating firewalls around decisions such that the decision can be changed without that change cascading into the rest of the system. See Parnas 1971.


    There are some mistake detection benefits as well. Consider

    todays_order.userId + yesterdays_order.userId
    

    That's utter nonsense code; adding two identifiers together doesn't do anything useful. But adding to Long values together is a perfectly normal thing to do in other contexts, and the compiler isn't going to catch this mistake.

    recindOrder(orderId, userId)
    

    Did you catch the bug? I've got the arguments in the wrong order! When the method signature is

    recindOrder(Long userId, Long orderId)
    

    The machine can't help me catch the problem, because I haven't given it the hints that it needs to look beyond the data structures.


    There is also a theory that by providing an explicit representation of the domain value, that code attracts other related functions that otherwise might not find a home -- in effect, it improves the coherence of your design.

    (In my experience, that's less true of semantically opaque types like identifiers than it is for numerical abstractions like money. However, if you have some identifiers that are reserved, then the identifier type becomes a convenient place to document the reservation.)