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?

New contributor
danwag is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.

2 Answers 2

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
    Aug 3 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? Aug 3 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
    Aug 3 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. Aug 4 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
    Aug 4 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. Aug 4 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
    Aug 5 at 6:23

Your Answer

danwag is a new contributor. Be nice, and check out our Code of Conduct.

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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