I have class of Items. These items can have a parent and subitems, which can also have subitems and so on recursively in a tree structure. The price of the Items should be the sum of the prices of all descendant Items. The items at the bottom of the tree structure already have price assigned and are available through the repository.
The repository is used to access a database where the data is structured with columns (Id, Name, ParentId, ChildIds, Price), though I'd prefer not to use SQL to implement domain logic. That's why I'm looking to implement the calculation of the price in the Item class of my domain model, but to perform a recursive search to get all descendants of an item, the item object needs access to a repository Class instance.
The class currently looks like this:
public item(string id, string name, string parentId, List\<string\> childIds)
{
this.Id = id;
this.Name = name;
this.ParentId = parentID;
this.ChildIds = childIds;
}
public string Id { get; init; }
public string Name { get; init; }
public string ParentId { get; init; }
public List<string> ChildIds{ get; init; }
public int Price { get; init => CalculatePrice(this.Id) }
public int CalculatePrice(string id) {
list<Item> descendantItems = _itemsRepository.GetAllDescendants(id);
return descendantItems.Select(i => i.Price).Sum()
}
The only options I see to get access to my repository class both look pretty bad :
An alternative I see is to have a field for the list of descendant items and fill it when my item is constructed but it seems like a waste to have all descendant items constructed and available in a field just because I need to assign a price to an item.
The final alternative I'm currently working on is to just get the Sum of the prices of all descendants by using a recursive SQL query in my mapper and to pass the resulting SumPrice to the constructor of the Item. This solves my problem, but it seems counter to Domain Driven Design to implement this business logic in an SQL Query in my Persistence layer.
This is a good example of the domain model purity vs completeness vs performance trilemma.
Ideally, the domain model should be pure, free of out of process (e.g. repository) dependencies.
In order to achieve this you can resolve dependencies in the application layer and provide dependencies through arguments. This sacrifices completeness slightly, and performance.
Item item = itemRepository.itemOfId(itemId);
List<Item> descendentItems = itemRepository.allDescendentsOf(itemId);
Money itemPrice = item.calculatePrice(descendantItems);
Ideally, the domain model should be complete. This means that we'd like all business logic to be part of the domain model. In the above example, the domain is mostly complete, but there's still domain logic leakage, like how to resolve descendants. If we'd want to improve completeness maybe we'd have something like the following, which sacrifices performance & purity.
// Pseudo-code
Money calculatePrice(ItemRepository repo) {
if (this.childIds.isEmpty()) return this.price;
return this.childIds.map(c -> c.calculatePrice(repo)).sum();
}
Ideally, the domain model (and application) should be performant. If we look at the above examples #2 is certainly not performant as there's many DB calls and #1 could possibly be acceptable depending on the number of descentants, but still loads much more data in memory than needed.
To improve performance maybe we'd want to do the entire calculation with a recursive SQL CTE, but then we sacrifice completeness.
There's other alternatives as well obviously, such as leveraging domain events. You could cache the price on each items, but then recalculate when events such as ItemPriceChanged
, ChildItemRemoved
, etc happens.
In conclusion, we can see that we need to find the right balance in the trilemma. Furthermore and more importantly, we need to ensure correctness. Think about how concurrency can affect the business processes and ask domain experts what should happen in those cases. Challenge the rules/invariants by uncovering the actual risks & business impacts as often invariants are only artificial.