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 Recipe
s, 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
Name
s 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
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.
-
1yes, 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– danwag08/03/2022 15:11:28Commented 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?Robert Bräutigam– Robert Bräutigam08/03/2022 15:39:18Commented Aug 3, 2022 at 15:39
-
It's just a very simple design to demonstrate DDD. It's a
RecipeCollection
that holds a collection ofRecipe
, but I decided theRecipe.Name
should be unique in aRecipeCollection
- seems like a fairly common use case. I think changing theRecipeCollection.AddRecipe(RecipeId recipeId)
method to beAddRecipe(RecipeId recipeId, Func<RecipeId, RecipeCollectionId, bool> doesRecipeNameExistInCollection)
may be the best solution to ensure the business logic remains in theRecipeCollection
. The caller will be responsible for providing aFunc
to fulfil the query.danwag– danwag08/03/2022 15:47:02Commented 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.Bart van Ingen Schenau– Bart van Ingen Schenau08/04/2022 07:38:11Commented Aug 4, 2022 at 7:38
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 RecipeId
s 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.
-
Thanks @candied_orange. The problem I can foresee is that
Recipe
s may have duplicate names, just not within aRecipeCollection
. 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– danwag08/04/2022 15:22:01Commented 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
RecipeId
s you can hold other Id's. More importantly you can certainly hold the fields you need to enforce your constraint.candied_orange– candied_orange08/04/2022 17:25:19Commented 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– danwag08/05/2022 06:23:17Commented Aug 5, 2022 at 6:23
It will be very useful if you examine the following sample design that Microsoft has given as best practice.
Note the relationship between the Order and OrderItem tables.
-
Thank you for your answer. But, it still has the question, what happens if
OrderItem
is very large, and anOrder
may contain 100s or 1000s of them? In their example, where anOrder
holds all of theOrderItem
s, loading anOrder
could be very slow.danwag– danwag11/29/2022 16:20:10Commented 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-dataaog– aog12/03/2022 21:52:47Commented Dec 3, 2022 at 21:52
-
Can you give some more explanation? This looks like a link-only answer.Dominique– Dominique12/08/2022 13:23:47Commented 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=fhM0V2N1GpYaog– aog12/09/2022 21:10:39Commented Dec 9, 2022 at 21:10
Explore related questions
See similar questions with these tags.