kaz
kaz

Reputation: 103

Loading aggregates on reacting to domain events

I am implementing an application with domain driven design and event sourcing. I am storing all domain events in a DomainEvents table in SQL Server.

I have the following aggregates:

- City
   + Id
   + Enable()
   + Disable()

- Company
   + Id
   + CityId
   + Enable()
   + Disable()

- Employee
   + Id
   + CompnayId
   + Enable()
   + Disable()

Each one encapsulates its own domain logic and invariants. I designed them as separate aggregates, because one city may have thousands (maybe more) companies and company may also have very large number of employees. If this entities would belong to the same aggregate I had to load them together, which in most cases would be unnecessary.

Calling Enable or Disable will produce a domain event (e.g. CityEnabled, CompanyDisabled or EmployeeEnabled). These events contain the primary key of the enabled or disabled entity.

Now my problem is a new requirement forcing me to enable/disable all related Companies if a City is enabled/disabled. The same is required for Employees, if a Company is enabled/disabled.

In my event handler, which is invoked if for example CityDisabled has occurred I need to execute DisableCompanyCommand for each company belonging to that city.

But how would I know what companies should be affected by that change?

My thoughts:

  1. Querying the event store is not possible, because I can't use conditions like 'where CityId = event.CityId'

  2. Letting the parent know its child ids and putting all child ids in every event the parent produces. Is also a bad idea because the event creator shouldn't care who will consume the events later. So only information belonging to the happening event should be in the event.

  3. Executing the DisableCompanyCommand for every company. Only the companies having the matching CityId would change their state. Even though I would do that asynchronously it would produce a huge overhead loading every company on those events. And also for every company getting disabled the same procedure should be repeated to disable all users.

  4. Creating read models mapping ParentIds to ChildIds and loading the childIds according to the parentId in the event. This sounds like the most appropriate solution, but the problem is, how would I know if a new Company is created while I am disabling the existing ones?

I am not satisfied with any of the solutions above. Basically the problem is to determine the affected aggregates for a happened event.

Maybe you have better solutions ?

Upvotes: 3

Views: 737

Answers (3)

Pavel S.
Pavel S.

Reputation: 1224

It's quite probable that you don't have to explicitly force rules like 'enable/disable all related Companies if a City is enabled/disabled' in the domain (write) side at all.

If so, there's no need to disable all Companies when a City is disabled, within the domain. As Charles mentioned in his answer, just introduce a rule that e.g., "a Company is disabled if it is disabled itself (directly) or its City is disabled". The same with Company and its Employees.

This rule should be realized at the read side. A Company in the read model will have 2 properties: the first one is Enabled which is directly mapped from the domain; the second one is EnabledEffective which is calculatable based on the Company's Enabled value and its City's Enabled value. When a CityDisabled event happens, the read model's event handler traverses the City's all Companies in the read model and sets their EnabledEffective property to false; when a CityEnabled event happens, the handler sets the City's every Company's EnabledEffective property back to its own Enabled value. It is EnabledEffective property that you will use in the UI.

The logic can be a bit more complex with CompanyEnabled/CompanyDisabled event handling (in respect to Empoyees) as you must take into account both event info and enabled/disabled status of the host City.

If (effective) enabled/disabled status of a Company/Employee is really needed in the domain side (e.g. affecting the way these aggregates handle their commands), consider taking EnabledEffective value from the read side and passing it along with the command object.

Upvotes: 0

Charles
Charles

Reputation: 3774

Do your requirements mean you have to fire a CompanyDisabled event when disabling a city?

If not - and your requirement is just that a disabled city means all companies are disabled, then what you would do is on your city read model projection you'd listen for CityDisabled events and mark the companies disabled in your read model. (If your requirements are to fire an event for each city then Constantin's answer is good)

Your model is more of a child / parent relationship - its kind of a break in traditional "blue book" thought, but I recommend represent this relationship in your domain with more than a CityId.

In my app something like this would be coded as

public Task Handle(DoSomething command, IHandlerContext ctx) 
{
   var city = ctx.For<City>().Get(command.CityId);
   var company = city.For<Company>().Get(command.CompanyId);

   company.DoSomething();
}

public Company : Entity<City>
{

   public void DoSomething()
   {
        // Parent is the City
        if(Parent.Disabled)
            throw new BusinessException("City is disabled");

        Apply<SomethingDone>(x => {
            x.CityId = Parent.Id;
            x.CompanyId = Id;
            ...
        });
   }

}

(Psuedo code is NServiceBus style code and using my library Aggregates.NET)

Upvotes: 0

Constantin Galbenu
Constantin Galbenu

Reputation: 17683

What you are describing can be resolved by a Saga/Process manager that listen to the CityDisabled event. Then it finds the CompanyIds of all Companies in that City (by using an existing Read model or by maintaining a private state of CityIdsxCompanyIds) and sends each one a DisableCompany command.

The same applies to CompanyDisabled event, regarding the disabling of Employee.

P.S. Disabling a City/Company/Employee seems like CRUD to me, these don't seem terms from a normal ubiquitous language, it's not very DDD-ish but I consider your design as being correct in regard to this question.

Upvotes: 2

Related Questions