DDD Decoded - Domain Relationships Explained
One of the most trickiest things to understand in DDD is the domain relationship between 2 concepts. Most of the time when people think relationship they use the programmer mindset and they look for:
parent-child or (worse)
one-to-many etc relationships. Which is valid when you write code but absolutely wrong when you do domain modelling.
What we’re looking for is a Domain Relationship i.e how the Domain sees the relationship from a business point of view or, to be more precise, what is the level of association/partnership between the concepts and what business rules are involved in maintaining the relationship. And usually there are 2 types: strong/always and weak/optional.
Strong (always) domain relationships
This one is the easiest: it simply means that in a given business case, a concept always needs the other. Examples: Customer and Order, Author and Tutorial etc. Contrary to how many devs think, it’s never “Customer has orders”, it’s always an action about an
Order which might involve or not the
Customer. When the customer places an order, we need to create that order and we’re going to use a specific
Order aggregate. But orders don’t just appear from thin air, a customer is always involved so we can’t create a new order without mentioning the customer.
Order concept requires at its creation a
Customer so, in our Order model, we need to find the representation of the Customer that will be used by the Order aggregate. In most cases, we only need the simplest model that represents the existence of the Customer, so we’ll use its id. That id is a Value Object and will be a part of the Order aggregate. We don’t care about a Customer aggregate, because we don’t need one for this business case. The
Order is the main concept,
Customer is secondary.
However, if we are in a “Cancel order” case, there is no
Customer-Order relationship to care about (there is an
Order-OrderStatus one, though). So, we can cancel the Order without involving/mentioning the Customer, but, as I’ve said, there’s a different relationship to take care of now.
Let’s say that an Order always needs to have a status. It might be tempting to include it as part of the aggregate but… if you look at it like the business does, it makes no sense for this Value Object to be part of it. What makes sense is to consider the
Order Status as being a sort of metadata associated with an Order.
When the Order is created, the only place where this relationship should be represented is the resulting
OrderCreated event. That’s because a status like “Pending” is implicit, we don’t need any particular input or business constraint for it. But we still need to ‘announce’ that the order has that status, because it is part of the business state change.
Now, when cancelling an order, we need to specify a different status for the order, however because we can have different outcomes based on the existing order status, we need to identify an aggregate just for that. This aggregate involves the Entity
Order (via OrderId) and the Value Object
OrderStatus and while being a model specific to changing the order status (representing the relationship itself) the resulting change is still associated with the
Using Event Sourcing, this means that the
OrderCanceled event is considered to be part of the
Order and so, the entity’s id is the order’s id. But the aggregate is about the relationship between
OrderStatus, it’s not just an Order aggregate. If this sounds weird, I can only say that it will make sense with more experience.
Weak (optional) domain relationships
Basically, 2 concepts can be associated together. Think
Category. Both concepts are independent, but they can work together. In the business case “Assign post to category” we need to create the association between them according to business rules. The relationship aggregate will involve both concepts and we get an interesting situation, because we end up with 2 entities but we need only one to act as the aggregate root. Which will it be?
Clearly, we shouldn’t just flip a coin. We need to understand the concepts better as well as the purpose of their association. It turns out that
Category exists to organize posts, while a
Post really doesn’t care about categories. This relationship is more important (relevant) for the
Category so, we select it to act as the aggregate root. This means that in the resulting event, the entity id would be that of the
PostId will be just a field.
Being a relationship, even if changes are considered to ‘belong’ to a specific concept, the aggregate is always a model for the relationship.
An interesting example is deleting something. If I soft delete a post, what I have is in fact a new relationship between the
Deleted status, the status is never part of the post itself (as a concept) but always some metadata associated with the post. Btw, the status is a Value Object even if its implementation is just an enum.
Another example can be “Pay invoice”. Yet again, a relationship between an Entity (
Invoice) and a Value Object (
InvoiceStatus) . The status is metadata for the invoice and we need a relationship aggregate to change the status. From the domain point of view, something associated with the Invoice entity has changed.
The hardest part about domain relationships is to properly identify them; many times, especially when an Entity and a Value Object are involved, we can think that the VO is part of the Entity’s aggregate, when in fact it’s a relationship that might need its own aggregate. But with more practice, spotting relationships will become second nature.