Suppose I have a domain service which implements the following business rule / policy:
If the total price of all products in category 'family' exceeds 1 million, reduce the price by 50% of the family products which are older than one year.
Using collection-based repositories
I can simply create a domain service which loads all products in the 'family' category using the specification pattern, then check the condition, and if true, reduce the prices. Since the products are automatically tracked by the collection-based repository, the domain service is not required to issue any explicit infrastructure calls at all – as should be.
Using persistence-based repositories
I'm out of luck. I might get away with using the repository and the specification to load the products inside my domain service (as before), but eventually, I need to issue Save
calls which don't belong into the domain layer.
I could load the products in the application layer, then pass them to the domain service, and finally save them again in the application layer, like so:
// Somewhere in the application layer:
public void ApplyProductPriceReductionPolicy()
{
// make sure everything is in one transaction
using (var uow = this.unitOfWorkProvider.Provide())
{
// fetching
var spec = new FamilyProductsSpecification();
var familyProducts = this.productRepository.findBySpecification(spec);
// business logic (domain service call)
this.familyPriceReductionPolicy.Apply(familyProducts);
// persisting
foreach (var familyProduct in familyProducts)
{
this.productRepository.Save(familyProduct);
}
uow.Complete();
}
}
However, I see the following issues with this code:
FamilyProductsSpecification
) and the policy is lost, essentially allowing someone to pass the wrong products into the domain service. Note that filtering the products (in-memory) again in the domain service does not help either, as the caller might have passed only a subset of all products.Question: Is there a better strategy to deal with this situation?
I thought about something complicated like adapting the persistence-based repository such that it appears as a collection-based one to the domain service, internally keeping track of the products which were loaded by the domain service in order to save them again when the domain service returns.
First of all, I think choosing a domain service for this kind of logic - which does not belong inside one specific aggregate - is a good idea.
And I also agree with you that the domain service should not be concerned with saving changed aggregates, keeping stuff like this out of domain services also allows you to be concerned with managing transactions - if required - by the application.
I would be pragmatic about this problem and make a small change to your implementation to keep it simple:
// Somewhere in the application layer:
public void ApplyProductFamilyDiscount()
{
// make sure everything is in one transaction
using (var uow = this.unitOfWorkProvider.Provide())
{
var familyProducts = this.productService.ApplyFamilyDiscount();
// persisting
foreach (var familyProduct in familyProducts)
{
this.productRepository.Save(familyProduct);
}
uow.Complete();
}
}
The implementation in the product domain service:
// some method of the product domain service
public IEnumerable<Product> ApplyFamilyDiscount()
{
var spec = new FamilyProductsSpecification();
var familyProducts = this.productRepository.findBySpecification(spec);
this.familyPriceReductionPolicy.Apply(familyProducts);
return familyProducts;
}
With that the whole business logic of going through all family products older than a year and then applying the current discount (50 percent) is encapsulated inside the domain service. The application layer then is again only responsible for orchestrating that the right logic is being called in the right order. The naming and how generic you want to make the domain service methods by providing parameters might of course need tuning, but I usually try to make nothing too generic if there is only one specific business requirement anyway. So if that's the current family product discount I would than already know where exactly I need to change the implementation - in the domain service method only.
To be honest, if the application method is not getting more complex and you don't have different branches (such as if conditions) I would usually start off like you originally proposed as the application layer method also simply makes calls to domain services (in your case the repository) with the corresponding parameters and has no conditional logic in it. If it get's more complicated I would refactor it out into a domain service method, e.g. the way I proposed.
Note: As I don't know the Implementation of FamilyPriceRedcutionPolicy I can only assume that it will call the corresponding method on the product aggregates to let them apply the discount on the price. E.g. by having a method such as ApplyFamilyDiscount() on the Product aggregate. With that in mind, considering that looping through all the products and calling the discount method will be only logic outside the aggregate, having the steps of getting all products from the repository, calling the ApplyFamilyDiscount() method on all products and saving all changed products could indeed just reside in the application layer.
In terms of considering domain model purity vs. domain model completeness (see discussion below concerning the DDD trilemma) this would move the Implementation again a little more in the direction of purity but also makes a domain service questionable if looping through the products and calling the ApplyFamilyDiscount() is all it does (considering the fetching of the corresponding products via the repository is done beforehand in the application layer and the product list is already passed to the domain service). So again, there is no dogmatic approach and it is rather important knowing the different options and their trade-offs. For instance, one could also consider to let the product always calculate the current price on demand by applying all applicable possible discounts when asking for the price. But again, if such a solution would be feasible depends on the specific requirements.