Search code examples
domain-driven-design

DDD: how should I check state of related aggregates inside one usecase?


Let's say I have three aggregates in the same bounded context:

  1. PhoneAggregate
  2. ServiceCenterAggregate
  3. ServiceWorkAggregate

ServiceWorkAggregate references both PhoneAggregate and ServiceCenterAggregate by id.

Now when new ServiceWorkAggregate is created, a POST request is sent:

{
    "phone_id": uuid,
    "servicecenter_id": uuid
}

I can't just create a new aggregate using those IDs, I need to perform some checks: at least confirm that those UUIDs are valid entity IDs, maybe also perform some business logic checks involving current state of selected PhoneAggregate and ServiceCenterAggregate. What is the best way to do this?

  1. I could make ServiceWorkAggregate application service or domain service include repositories for all three aggregates.
  • if I go this route, should application service just pass IDs into domain service and domain service would load aggregates and do all the checks, since there could be business login involved.
  • or should application service fetch aggregates and pass them into domain service?
  1. I could treat referenced aggregates as value objects and instead of phone_id and servicecenter_id fields inside ServiceWorkAggregate use value objects Repairable and ServiceLocation, which would be immutable copies of those aggregates, fetched from the same table etc. Then I would need only one repository in my servicework application service.
  2. Is there a better way?

Solution

  • A lot of developpers think that the domain layer of an application must be designed as a single normalized data model, like an RDBMS schema. This is not true, and is the cause of many suffering.

    The fact that you have 3 different contexts does not mean that the ServiceWork context is not able to manipulate data from the other two concepts. It only means that this context is not able to authoritatively change these concepts states. Put in other words, ServiceWorkRepository can read data about the Phone context, but it cannot create, delete or change a phone's state.

    Also, you don't need the same information regarding a phone's state when you are changing a phone's context, or when you are changing a service work's context. This allows you to have two different models for the same concept, depending on the current context. Let's call them ServiceWorkContext.PhoneEntity and PhoneContext.PhoneEntity. These entities can map to the same table/columns in the database, but they can have different level of details: flattened relationships, fewer columns read, and do not allow changing the state.

    This concept is called polysemic domain modeling.

    In other words, you have the following classes:

    • MyApp.Domain.PhoneContext.PhoneAggregate
    • MyApp.Domain.PhoneContext.PhoneRepository
    • MyApp.Domain.ServiceCenterContext.ServiceCenterAggregate
    • MyApp.Domain.ServiceCenterContext.ServiceCenterRepository
    • MyApp.Domain.ServiceWorkContext.PhoneEntity
    • MyApp.Domain.ServiceWorkContext.ServiceCenterEntity
    • MyApp.Domain.ServiceWorkContext.ServiceWorkAggregate
    • MyApp.Domain.ServiceWorkContext.ServiceWorkRepository

    MyApp.Domain.PhoneContext.PhoneAggregate represents the phone's state when you want to create or update a phone, and validates business rules regarding these use casses.

    MyApp.Domain.ServiceWorkContext.PhoneEntity represents a simplified readonly copy of the phone's state, used when trying to create / update a service work. For instance it can contain a single Country property (which maps to Phone->Owner->Address->Country->Name) to be compared with the ServiceCenter's country, if you have a business rule stating that the phone's country must match the service center's. In that case, you may not need to read Phone.Number from the database for instance.