Home > database >  Enforce business rules out of Aggregate
Enforce business rules out of Aggregate

Time:11-11

Currently I am learning DDD and CQRS approaches. I started a project, related to restaurant domain.

One of modules which I found (using Event Storming) is Menus. In the Menus module, the manager of restaurant can store multiple Menus. Menu is AggregateRoot, which enforce rules between groups and connected items (positions) in menu.

Problematic Rules:

  1. One Menu in Restaurant can be active at once.
  2. Internal names of Menus must be unique within the Restaurant.

To enforce that rules I try to pass Restaurant repository interface to the Menu (I focused more on 1. rule, because 2. is even harder). I feel that I am doing something wrong. I had an idea of using domain service to enforce that and after analysis I don't think it will be better. Do you have any tips for me? Thank you :)

public class Menu : AggregateRoot<MenuId>
{
    public string InternalName { get; private set; }
    
    public RestaurantId RestaurantId { get; private set; }

    public IReadOnlyList<Group> Groups => _groups;
    private List<Group> _groups = new();

    ...

    internal void Activate()
    {
        CheckRule(new ActiveMenuMustHaveAtLeastOneGroup(_groups));
        
        _groups.ForEach(x => x.CheckConsistency());
    }

    public void ChangeInternalName(string newInternalName, IRestaurantRepository 
         restaurantRepository)
    {
        CheckRule(new InternalNameMustBeUniqueInRestaurantMenusRule(RestaurantId, 
             newInternalName, restaurantRepository));
        InternalName = newInternalName;
    }
}

public class Restaurant : AggregateRoot<RestaurantId>
{
    public MenuId ActiveMenuId { get; private set; }

    public IReadOnlyList<MenuId> MenuIds => _menuIds;
    private List<MenuId> _menuIds = new();

    ...

    public void ChangeActiveMenu(MenuId newActiveMenuId, IMenuRepository menuRepository)
    {
        CheckMenuExists(newActiveMenuId);
        CheckRule(new CannotActivateActiveMenuRule(ActiveMenuId, newActiveMenuId));
        
        var menu = menuRepository.GetAsync(newActiveMenuId).Result;
        menu.Activate();
        ActiveMenuId = newActiveMenuId;
    }

    ...
}

CodePudding user response:

Ok, so let's try to handle it from the top - and please, mind, that my answer is >opinion based<, as is most of design decisions.

For the first question:
It's Restaurant's job to assign active menu, so this aggregate should hold a louse coupling to Menu - let's say it stores it's ID. Only one ID for that matter.
When selecting new menu to activate, Restaurant AR can easily check if it's the current id or not, without need for repository.
Now, if given menu id is different, then Restaurant should have a way to inform Menu context about the change - is this a good thing, that menu knows if its active - that's whole other thing.

So, here comes CQRS with event handlers - Restaurant should emit an MenuChanged event, and this event handler should be responsible for updating menu aggregate.
Therefore, you should fire up domain event in your ChangeActiveMenu method, that will be fired up when the aggregate is persisted (typically by repository doing the persistence).

    public void ChangeActiveMenu(MenuId newActiveMenuId)
    {
        CheckRule(new CannotActivateActiveMenuRule(ActiveMenuId, newActiveMenuId));
        ActiveMenuId = newActiveMenuId;
        this.EventStore.Add(new MenuChangedEvent(newActiveMenuId));
    }

Now the job of activating menu goes to the MenuChangedEvent handler - wiring this up is done with popular MediatR library often.

Second question: Many ways of checking the uniqueness of the name across the aggregate roots are possible.
One thing is sure - aggregate should not force invariants outside of it's bounds. So, in this case, invariant is a Name and crossing boundary would be to let Menu check across multiple aggregate instances for consistency.

For one, you could implement a domain service that would take the repository and check for uniqueness. Or You could create such service as an application layer service even.

Let's not forget to and / or delegate - if you use relational database, then why not implement a composite key, in this case for Menu made from RestaurantId and Name - then the invariance would be also forced by the db.
Get the unique constrain exception - aggregate is not saved, it's events are not sent etc. and you just need to handle it somewhere.

This would be my goto solution for this, other then just expanding Restaurant aggregate to contain Menu as a part of it.

Again, this is opinion based and there are many, many ways for tackling this problem.

  • Related