On my current project we've had a lot of success using a persistence ignorant domain with NHibernate and repositories being used to manage persistence.
The domain assembly does not reference the persistence classes in any way which has made the domain very easy to unit test but it does come at a cost.
Lets give you one example of a problem this persistence ignorance creates.
We have a Customer which has multiple Orders. Each Customer has a default FeeGroup and each new Order associated with the Customer must go in the FeeGroup to ensure that any fees are correctly used (we can later move an Order into another FeeGroup as necessary).
Our first solution might be to have an association from Customer to FeeGroup. The transitive persistence functionality in NHibernate would handle the persistence for us.
The problem with this is that Customer is a core class and FeeGroup most certainly isn’t. To emphasize this lets that in fact Customer is in CoreAssembly and FeeGroup is in FinanceAssembly, FinanceAssembly is used by a small number of applications whilst CoreAssembly is used by lots of applications.
In this case we will probably want to avoid CoreAssembly referencing FinanceAssembly, otherwise everytime we add a reference to CoreAssembly we'll have to add a reference to FinanceAssembly too.
This does however leave us with a problem. If a Customer does not know about its default FeeGroup, and if we want all new Orders to go in to the default FeeGroup, then how does the domain look up the default fee group?
This is what we need code to do:
- Look up the default fee group for a client using the FeeGroupRepository (Evans repository pattern).
- If the Customer has no default FeeGroup then create one, use the FeeGroupRepository to save it and then assign it to the Customer.
- Pass all the data needed to create the Order, along with the FeeGroup, into the OrderFactory.
We need somewhere to put this code. The problem is that if this logic is in the process layer then other applications cannot reuse it. It is also confusing that the FeeGroup is passed to the OrderFactory, the only way we can specify that it should be the Customers default FeeGroup is using a comment on that method. Plus we've now coupled our core Customer class to FeeGroup which is unacceptable.
The solution was to add a static OrderCreationService class. The OrderCreationService has a single static CreateOrder method that has all the logic specified above, so it lookups or creates the FeeGroup as required, then uses the OrderFactory domain class to create the Order before finally adding to the FeeGroup. It then calls into an internal method on the OrderFactory which will create the new Order.
This works well because the OrderCreationService can be in the same namespace as Customer/Order. Users will thus find it quite easily.
However the question is which assembly should we put OrderCreationService into. It can't go in the process/application layer as I don't want to move domain logic (even coordination logic) out of the domain and I also wan't the coordination logic to be used by all applications. It also can't go in our persistence ignorant domain because that will couple my domain assemblies to persistence and to each other in ways that I want to avoid.
The solution was to create new Domain.Coordination layer/assembly. This think assembly provides a coarse grained interface, with an emphasis on static classes and services/factories needed to organise the domain classes kept in the domain assemblies.
There are definitely disadvantages to my solution, one is that the Domain.Coordination layer is coupled to CoreAssembly, FinanceAssembly and any persistence assemblies involved. There isn't any easy way to avoid this, something has to be coupled to those assemblies.
Anyway although it adds a bit of complexity it has several advantages:
- Helps decouple our domain assemblies (at the cost of coupling the domain coordination layer itself).
- Helps us maintain the persistence ignorance which is important as it keeps the domain simple and makes it more testable.
- Ensures that coordination logic will be accessible by all applications that use our domain.
- Since the classes in the domain coordination layer are in the same core namespace as the other domain classes people will locate them easily.
I chose the term domain coordination as I think it emphasizes what the layer is there for.