How should I enforce relationships and constraints between aggregate roots?
Asked Answered
W

2

14

I have a couple questions regarding the relationship between references between two aggregate roots in a DDD model. Refer to the typical Customer/Order model diagrammed below.

enter image description here

First, should references between the actual object implementation of aggregates always be done through ID values and not object references? For example if I want details on the customer of an Order I would need to take the CustomerId and pass it to a ICustomerRepository to get a Customer rather then setting up the Order object to return a Customer directly correct? I'm confused because returning a Customer directly seems like it would make writing code against the model easier, and is not much harder to setup if I am using an ORM like NHibernate. Yet I'm fairly certain this would be violating the boundaries between aggregate roots/repositories.

Second, where and how should a cascade on delete relationship be enforced for two aggregate roots? For example say I want all the associated orders to be deleted when a customer is deleted. The ICustomerRepository.DeleteCustomer() method should not be referencing the IOrderRepostiory should it? That seems like that would be breaking the boundaries between the aggregates/repositories? Should I instead have a CustomerManagment service which handles deleting Customers and their associated Orders which would references both a IOrderRepository and ICustomerRepository? In that case how can I be sure that people know to use the Service and not the repository to delete Customers. Is that just down to educating them on how to use the model correctly?

Wilscam answered 24/5, 2011 at 23:58 Comment(0)
S
11

First, should references between aggregates always be done through ID values and not actual object references?

Not really - though some would make that change for performance reasons.

For example if I want details on the customer of an Order I would need to take the CustomerId and pass it to a ICustomerRepository to get a Customer rather then setting up the Order object to return a Customer directly correct?

Generally, you'd model 1 side of the relationship (eg., Customer.Orders or Order.Customer) for traversal. The other can be fetched from the appropriate Repository (eg., CustomerRepository.GetCustomerFor(Order) or OrderRepository.GetOrdersFor(Customer)).

Wouldn't that mean that the OrderRepository would have to know something about how to create a Customer? Wouldn't that be beyond what OrderRepository should be responsible for...

The OrderRepository would know how to use an ICustomerRepository.FindById(int). You can inject the ICustomerRepository. Some may be uncomfortable with that, and choose to put it into a service layer - but I think that's overkill. There's no particular reason repositories can't know about and use each other.

I'm confused because returning a Customer directly seems like it would make writing code against the model easier, and is not much harder to setup if I am using an ORM like NHibernate. Yet I'm fairly certain this would be violating the boundaries between aggregate roots/repositories.

Aggregate roots are allowed to hold references to other aggregate roots. In fact, anything is allowed to hold a reference to an aggregate root. An aggregate root cannot hold a reference to a non-aggregate root entity that doesn't belong to it, though.

Eg., Customer cannot hold a reference to OrderLines - since OrderLines properly belongs as an entity on the Order aggregate root.

Second, where and how should a cascade on delete relationship be enforced for two aggregate roots?

If (and I stress if, because it's a peculiar requirement) that's actually a use case, it's an indication that Customer should be your sole aggregate root. In most real-world systems, however, we wouldn't actually delete a Customer that has associated Orders - we may deactivate them, move their Orders to a merged Customer, etc. - but not out and out delete the Orders.

That being said, while I don't think it's pure-DDD, most folks will allow some leniency in following a unit of work pattern where you delete the Orders and then the Customer (which would fail if Orders still existed). You could even have the CustomerRepository do the work, if you like (though I'd prefer to make it more explicit myself). It's also acceptable to allow the orphaned Orders to be cleaned up later (or not). The use case makes all the difference here.

Should I instead have a CustomerManagment service which handles deleting Customers and their associated Orders which would references both a IOrderRepository and ICustomerRepository? In that case how can I be sure that people know to use the Service and not the repository to delete Customers. Is that just down to educating them on how to use the model correctly?

I probably wouldn't go a service route for something so intimately tied to the repository. As for how to make sure a service is used...you just don't put a public Delete on the CustomerRepository. Or, you throw an error if deleting a Customer would leave orphaned Orders.

Succor answered 25/5, 2011 at 0:40 Comment(7)
Thanks! This really helps a lot. However, say one side of a relationship is added to the model as you describe such as an Order.Customer property. Wouldn't that mean that the OrderRepository would have to know something about how to create a Customer? Wouldn't that be beyond what OrderRepository should be responsible for, and thus break the "Law of Demeter" (bit.ly/smi5M). The answer to this question (bit.ly/iuVq2k) was what originaly made me think that references to arggregate roots should only be saved as IDs and not object references.Wilscam
I have a question closely related to the one asked above, Suppose for one side of the relationship I do something like Customer.Orders now the Order will be lazily loaded, however what about the dependencies which a repository in this case OrderRepository would have loaded ? For example if the OrderRepository would inject a EventPublisher in each Order AR, this would get missed out in this scenarioMarlie
@Marlie - the CustomerRepository can (and should) use the OrderRepository to load the Order entity. That way, you're not duplicating the logic between CustomerRepository and OrderRepository. If Order is an aggregate root as well, then the OrderRepository should be publically accessible and the FindOrdersByCustomerId method internal. If it's not an aggregate root, then it only needs to be internal or private to the CustomerRepository - since external clients aren't allowed to have a direct reference to a non-aggregate entity.Succor
Thanks Mark, Got what you where saying, I have a implementation specific question now :), Assuming both Customer and Order are Aggregate Roots, The Customer class has a reference to Set<Order> orders, So 1 customer can have many orders,When i look up a customer I lazy load all his orders as well, However because i want to ensure that all the dependecies of Order AR are properly set , In the customerRepository itself I should OrderRepository.FindOrdersByCustomerId and then do something like customer.setOrders(orderRepository.FindOrdersByCustomerId(2)) ... Is this a fair approach ?Marlie
Hi ... I am similar discussion is taking place at groups.google.com/group/dddcqrs/browse_thread/thread/… ......... however it seems like there is a fair difference in the approach being suggested in those forums ... any ideas ??Marlie
@Marlie - Yes - it seems at Google Groups they are suggesting Iulian's identifier value object approach. I don't see the point in abstracting that away myself, though (perhaps I need to do some reading). The approach you identified, with customer.setOrders(orderRepository.FindOrdersByCustomerId(2)) is pretty much exactly how I've always done it. Use lazy loading if needed for perf reasons (though you should take care that your aggregates don't get too large, and make all associations unidirectional to avoid cyclical references).Succor
rogeralsing.com/2009/11/08/two-flavors-of-ddd this hits the nail bang on the head :) .... it basically put the things spoken on this post very nicelyMarlie
G
2

Another option would be to have a ValueObject describing the association between the Order and the Customer ARs, VO which will contain the CustomerId and additional information you might need - name,address etc (something like ClientInfo or CustomerData).

This has several advantages:

  1. Your ARs are decoupled - and now can be partitioned, stored as event streams etc.
  2. In the Order ARs you usually need to keep the information you had about the customer at the time of the order creation and not reflect on it any future changes made to the customer.
  3. In almost all the cases the information in the value object will be enough to perform the read operations ( display customer info with the order ).

To handle the Deletion/deactivation of a Customer you have the freedom to chose any behavior you like. You can use DomainEvents and publish a CustomerDeleted event for which you can have a handler that moves the Orders to an archive, or deletes them or whatever you need. You can also perform more than one operation on that event.

If for whatever reason DomainEvents are not your choice you can have the Delete operation implemented as a service operation and not as a repository operation and use a UOW to perform the operations on both ARs.

I have seen a lot of problems like this when trying to do DDD and i think that the source of the problems is that developers/modelers have a tendency to think in DB terms. You ( we :) ) have a natural tendency to remove redundancy and normalize the domain model. Once you get over it and allow your model to evolve and implicate the domain expert(s) in it's evolution you will see that it's not that complicated and it's quite natural.

UPDATE: and a similar VO - OrderInfo can be placed inside the Customer AR if needed, with only the needed information - order total, order items count etc.

Gooseberry answered 25/5, 2011 at 8:6 Comment(4)
Yes I keep wanting my objects to correspond to tables, and I'm forgetting that with DDD the design of the model is not necessarily always a 1 to 1 match to the database. In your example does the Customer VO neccisarly need to be stored in the "Orders" table in the db? Or could the implementation of the OrderRepository access the Customers table to create the Customer VO? Say if I don't want to keep the customer information at the time of the order, but always want the latest customer information?Wilscam
In general the Customer VO is stored with the order, in the Orders table or in a separate table that has an order id FK. If you want the latest customer information, most probably you should review your model to see why you need it and look for a missing concept. If you can't find a missing concept than the OrderRepository might read it from the customer table. Or you could have an event handler that updates the customer information stored in the order when the information changes.Gooseberry
Sure whether the Order should have the latest customer information, or the current information at the time of the order is really a question of design. But am I right in thinking that as far as DDD is concerned it is ok for value types contained in separate aggregates to in reality be stored in the same database table? Would it be ok if Customer.Addresses and Order.ShippingAddress, both value types in the model, were actually stored in a single Addresses table? Say because in another context there may be a need to look up all the Orders or Customers for a specific address?Wilscam
Thinking about it i would say no, it's not ok. The reason is that ANY interactions and business logic should be explicitly modeled in your domain. You should not relay on the fact that the objects are persisted in the same table. Have the fact that Customer.Address and Order.ShippingAddress MUST have the same value explicitly modeled in the domain. Another issue is that VOs should (almost) all the time be immutable. So if the customer changes it's address you should have a new record added to that table - and it will not be easy to have the order reference the new record.Gooseberry

© 2022 - 2024 — McMap. All rights reserved.