domain-driven-designaggregateroot

DDD one to many relation between aggregate root


I am trying to model a domain for the backend of my app. In my domain, I have Cook aggregate root and every cook will have an Ethnicity. If we think in presistence layer ethnicity and cook has one to many relation ( one ethnicity can be assigned to multiple cooks). I want when cook is created. Admin will select Ethnicity from list of available ethnicities.if i create ethnicity entity type and fetch list of ethnicities this violate principle that any child of aggregate must not be accessed outside of root.

public class Cook : AggregateRoot<CookId>
{


    private Cook() : base(null)
    {
    }


    public Cook(CookId id, string name, string intro, Address address, string email) : base(id)
    {
        Name = name;
        Intro = intro;
        Address = address;
        Email = email;
        CreatedAt = DateTimeOffset.Now;
        ModifiedAt = DateTimeOffset.Now;
    }

    public readonly List<MealId> mealIds = new();
    public string Name { get; private set; }
    public string Intro { get; private set; }
    public Address Address { get; private set; }
    public int Phone { get; private set; }
    public string Email { get; private set; }
    public EthnicityId EthnicityId { get; private set; }
    public DateTimeOffset CreatedAt { get; private set; }
    public DateTimeOffset ModifiedAt { get; private set; }
    internal List<Review> _reviews = new();
    internal List<Meal> _meals = new();
    public IReadOnlyCollection<Review> Reviews => _reviews.AsReadOnly();
    public IReadOnlyCollection<Meal> Meals => _meals.AsReadOnly();

    public void AddReview(int rating, string comment)
    {
        _reviews.Add(Review.Create(rating, comment));
    }

    public void AddMeal(Meal meal)
    {
        _meals.Add(Meal);
    }
}

So, My question is should ethnicity be aggregate root itself and so it can be reference by ID in cook aggregate. This way CRUD operation can be performed on ethnicity repository.


Solution

  • Think about the lifetime of entities here. Does Ethnicity mean anything when there is no Cook? Does it have to be created only when a Cook needs it? These questions help you to recognize the aggregates and their boundaries. If the management of the Ethnicity goes through a Cook, it should be under the Cook aggregate. But from what I understood, in your case, it is separated from the Cook. So it would be best if you made it an aggregate root.

    if i create ethnicity entity type and fetch list of ethnicities this violate principle that any child of aggregate must not be accessed outside of root.

    There are some notes about this statement. Repositories in domain-driven design are not meant to be used as an interface for queries requested directly by the presentation layer. For that purpose, you can use a read model optimized for querying. But if you meant it for write-model, I think no use case needs to fetch a list of Ethnicities.

    Updated:

    In case you need just a value from storage to relate the Cook to ethnicity (no invariant no delete or update propagation just like an Enum type), just store it in any storage (it can differ from the Cook storage) and just pass the needed value to the Cook constructor method. For example, you can store the ethnicities in Redis and when you want to create a cook just pass the ethnicity data to the constructor. But if you are concerned about having update/delete side effects, it is reasonable to make an Aggregate Root for ethnicity.

    The UI requirement in general, will be met with CQRS and creating query interfaces that do not interfere with the domain models which is the write model. But in the above example, you wouldn't need to use CQRS to approach such problems. You just can read the ethnicities from Redis and select one of them. But if you want to define Ethnicity as an Aggregate Root, you can have a Query interface to query over ethnicities.

    public interface IGetAllEthnicitiesQueryHandler{
      IEnumerable<ReadModel.Entities.Ethnicity> GetAll();
    }
    

    with this QueryHandler you won't worry about loading the entire Aggregate. Query handlers are set to be read-only collections so no need to worry about managing write operations on them.

    Your repository would be like this if you choose to have Ethnicity as an Aggregate Root:

    public interface IEthnicityRepository{
      void Create(DomainModel.EthnicityAggregate.Ethnicity entity);  
    }
    

    Notice the models in these two interfaces are different. (DomainModel.EthnicityAggregate.Ethnicity VS ReadModel.Entities.Ethnicity)

    or the Cook model would be like this if you model the ethnicity as an entity in Cook Aggregate :

    public class Cook: IAggregateRoot
    {
      public Ethnicity Ethnicity {get; set;}
      public void SetEthnicity(Ethnicity ethnicity)
      {   
         Ethnicity = ethnicity;
      }
      public void CreateEthnicity(string name)
      {   
         Ethnicity = new Ethnicity (name);
      }
    }