1

I've come to a bit of an impasse with the best way to implement a DDD problem, and am hoping for some advice from those with more experience, please.

I have a RecipeCollection, which has a collection of type Recipe (as you might expect). The abbreviated code looks like this:

public class RecipeCollection : IEntity<RecipeCollectionId>, IAggregateRoot
{
    private readonly RecipeCollectionId id = new();
    private readonly List<RecipeId> recipeIds = new();

    public void AddRecipe(RecipeId recipeId) { /* not sure what to do here! */ }
...
}

public class Recipe : IEntity<RecipId>
{
    public readonly string Name { get; private set; }
...
}

One of the business rules is that a Recipe cannot have the same Name within a RecipeCollection. The obvious place to enforce this invariant is within RecipeCollection, when adding a new Recipe. However, RecipeCollection only holds a list of RecipeId, not a list of Recipe itself.

Ideally, RecipeCollection would hold a collection of Recipe, which would make checking for duplicate Recipe.Name easy. However, RecipeCollection holds a collection of RecipeId. It does this to avoid potential problems such as loading a RecipeCollection from memory, which loads all its Recipes, which each in turn load another collection, and so on, and before you know it, you've loaded the entire database. I know I could use lazy loading, but I may use a data store where this isn't possible, so I'm forced to hold a collection of RecipeId.

Is the best solution, in this case, to have the calling code, i.e. the command handler that retrieves the RecipeCollection from the repo, check there are no duplicated Recipe Names before adding the Recipe? This would leak the business logic out of the Aggregate root in this instance, which is why I haven't committed to this solution. Or is there another solution/pattern I'm missing?

3 Answers 3

2

Purely technically the obvious best solution is to let the database figure this out. Assuming there is a database and the number of recipes is unbounded, so we don't want to load all into memory or iterate every one. The database can probably already do this and probably much better.

If that is true, then we just have to figure out where the call should be. If it is up to the caller to do it, then obviously this logic is not encapsulated. Can be forgotten, duplicated, made poorly, etc.

I would expect it to be in AddRecipe(). It seemingly implements some use-case, so I would except it does everything necessary to fulfill it.

4
  • yes, good point, the database is best placed to recognise a duplicated recipe name. Normally, this would force the logic to live in the caller method, in the repo, or (if using an rdbms) by enforcing a unique constraint. Either way, the business logic is leaked. Your comment has got me thinking, though. The caller, which has access to the repository, could pass a callback to AddRecipe(), which would return true/false as to whether the name already exists in the collection. Then, AddRecipe() can perform the check before adding the recipe.
    – danwag
    Commented Aug 3, 2022 at 15:11
  • If some portion of the logic leaks, maybe the design is not optimal? I don't know the specifics of your requirements, but maybe another design may fit better? Commented Aug 3, 2022 at 15:39
  • It's just a very simple design to demonstrate DDD. It's a RecipeCollection that holds a collection of Recipe, but I decided the Recipe.Name should be unique in a RecipeCollection - seems like a fairly common use case. I think changing the RecipeCollection.AddRecipe(RecipeId recipeId) method to be AddRecipe(RecipeId recipeId, Func<RecipeId, RecipeCollectionId, bool> doesRecipeNameExistInCollection) may be the best solution to ensure the business logic remains in the RecipeCollection. The caller will be responsible for providing a Func to fulfil the query.
    – danwag
    Commented Aug 3, 2022 at 15:47
  • @danwag, it is not uncommon that uniqueness business requirements "leak" to the repositories/database, because databases have the best tools to enforce those constraints with the least amount of overhead. Commented Aug 4, 2022 at 7:38
1

Or is there another solution/pattern I'm missing?

You can push this problem out to the DB. But now your solution is dependent on the DB. And maybe that's fine. But there are alternatives.

I'm forced to hold a collection of RecipeId.

Remember, references are tiny. Doesn't matter how big your recipe object is you can copy references to it into many data structures and still only have one copy of the recipe loaded into memory.

Which means just because you have a collection of RecipeIds that may have duplicates doesn't mean that's the only collection you can have. Create Recipes and never let it get loaded or added to without validating. You can still have your other collection that doesn't check for whatever technical reason. But if Recipes is populated you know it's validated. You know there are no duplicate names in it.

With this approach you are free to store your recipes in text files because the bulk of your code doesn't know or care if there is the DB.

3
  • Thanks @candied_orange. The problem I can foresee is that Recipes may have duplicate names, just not within a RecipeCollection. It may be a bit of a contrived example, but I'm sure it's a common use case to need to check some property of a child entity against other child entities / value objects.
    – danwag
    Commented Aug 4, 2022 at 15:22
  • @danwag it is a common use case. You seem to think every Recipe loaded must be a fully resolved deep copy. But if you can hold RecipeIds you can hold other Id's. More importantly you can certainly hold the fields you need to enforce your constraint. Commented Aug 4, 2022 at 17:25
  • I am having a hard time picturing how to have a Recipe collection of Recipes that aren't fully resolved. Could you point me to an example, please? Or, are you suggesting RecipeCollection would hold a collection of Recipe.Name for the specific purpose of addressing the duplicate name problem?
    – danwag
    Commented Aug 5, 2022 at 6:23
0

It will be very useful if you examine the following sample design that Microsoft has given as best practice.

eShopOnContainers Github Repo

Note the relationship between the Order and OrderItem tables.

4
  • Thank you for your answer. But, it still has the question, what happens if OrderItem is very large, and an Order may contain 100s or 1000s of them? In their example, where an Order holds all of the OrderItems, loading an Order could be very slow.
    – danwag
    Commented Nov 29, 2022 at 16:20
  • I think you can solve the problem by using data loading patterns. Please consider following page: learn.microsoft.com/en-us/ef/core/querying/related-data
    – aog
    Commented Dec 3, 2022 at 21:52
  • Can you give some more explanation? This looks like a link-only answer.
    – Dominique
    Commented Dec 8, 2022 at 13:23
  • This is a very broad topic and difficult to explain with an example. I suggest you take your time for courses on youtube. youtube.com/watch?v=fhM0V2N1GpY
    – aog
    Commented Dec 9, 2022 at 21:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.