Domain-Driven Design – How to Make Conditional Queries Within DDD Logic

cqrsdomain-driven-design

I have created a small ERP application and the architecture I chose included the Repository pattern and the anemic Domain model and Business logic layer.
I was reading Microsoft's guide about microservices here:

https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/net-core-microservice-domain-model

and I saw some examples like this

https://github.com/dotnet-architecture/eShopOnContainers/blob/main/src/Services/Ordering/Ordering.API/Application/Commands/ShipOrderCommandHandler.cs

and others on various sites.
My understanding is that anemic model and BusinessLayer in large systems is an antipattern and logic should reside in the Domain model. Though the model should not (and cannot) query the Database but rather all needed information should be provider as parameters and queried at the CommandHandler (if I m not mistaken). But in some cases the data I need to fetch from the database is needed conditionally.
Take this example for instance

public void CalculateDiscount(CommercialDocumentEntry enrty,CommercialDocumentEntryLine line)
{
  var customer = RepositoryProvider.CustomerRepositrory.FindbyId(enrty.BillingTraderId);
  if (customer.ZoneId.HasValue)
  {
    var zone = RepositoryProvider.ZoneRepositrory.FindbyId(customer.ZoneId.Value);
    line.TDiscountItemPercent = zone.Discount;
  }
  else if (line.Item.PriceListId.HasValue)
  {
    var priceList = RepositoryProvider.PriceListRepositror.FindbyId(PriceListId.Value);
    line.TDiscountItemPercent = priceList.Discount;
  }
  else
  {
    var finData= RepositoryProvider.TraderFinancialDataRepository.FindbyId(enrty.BillingTraderId);
    if (finData.TurnOver > 1000)
    {
      var globalDicount = RepositoryProvider.GlobalDiscoutntsRepository.GetGlobalDiscountbyTurnOver(finData.TurnOver);
      line.TDiscountItemPercent = globalDicount.Value;

    }
    else if (enrty.BTotalValue>1000)
    {
      var valueDicount = RepositoryProvider.ValueDiscoutntsRepository.GetValueDiscountbyTotal(enrty.BTotalValue);
      line.TDiscountItemPercent= customer.IsLocal? valueDicount.LocalDiscount:valueDicount.ForeignDiscount;
    }

  }
}

If I decide to refactor my code and

  1. Place CalculateDiscount on the CommercialDocumentEntry aggregate
  2. Discard CommercialDocumentEntryBusinessLogic and call CalculateDiscount method from a DiacountCommandHandler
  3. Make all the calls to the database before calling the CalculateDiscount method from the DiacountCommandHandler and pass the results as parameters the CalculateDiscount method

Then I will have made 5 calls to the database in stead of 2. This will eventually deteriorate the performance of my application.

How do I tackle this problem with DDD and CQRS?

Thank you very much for your answers.

Best Answer

In short: introduce another level of indirection.

I would recommend to create a "helper service" (a.k.a service facade) which encapsulates the repo access and "inject" this into the entity's method. In your example above, one may create a service like DiscountProvider with methods GetZoneDiscount, GetGlobalDiscount, GetLocalDiscount and GetPricelistDiscount. These methods can internally do the repo access at the time when they are called (or in advance, or cache the result, this becomes an implementation detail). The service can be passed into CommercialDocumentEntry.CalculateDiscount method as a parameter. This way, the business logic is in your entity, but your entity is not really aware if it accesses a database to get the required data.

Note also just because you let some methods stay inside your service layer (and outside your entities), you don't automatically get an anemic domain model. If you have only a few methods with the issue you mentioned, I would probably spare the whole service facade overhead and let the method stay directly in some service, as shown in your post. On the other hand, maybe you own suggestion for refactoring, which involves making always five queries instead of two is quick enough? Did you measure it? Performance problems should be only solved when they have a measurable impact on your program.

Let me finally say, stay pragmatic here, write the code which needs to be written, and take DDD as a guideline, not as a law.