Search code examples
domain-driven-designaggregateroot

DDD child entity validation


Which layer should be responsible for checking existence of some entity in the database? Let's say I have an order as aggregate and that order can contain multiple items. Logic implies that I can add only existing items to order.

Should I write it like this in the application service:

var item = ItemRepository.GetByID(id);

//throws exception if the item is null
order.AddItem(item);

or

//validate item existence inside aggregate function
order.AddItem(item, IItemRepository repo);

Solution

  • Neither, really.

    Entities don't cross aggregate boundaries. Either the entity is part of the aggregate, in which case the aggregate manages its own life cycle, or the item is part of some other aggregate, in which case you don't share the entity, you share a reference.

    order.AddItem(id)
    

    Part of the definition of aggregate is that changes in different aggregates can happen independently of each other. In other words, there's no way that this aggregate can know what is happening in that aggregate "now".

    In other words, you can't ensure transactional consistency across an aggregate boundary.

    The right answer, if you are willing to accept a data race, is to use a domain service to query the state outside the boundary.

    interface InventoryService{
        boolean currentlyInStock(Item id);
    }
    
    // ...
    
    order.addItem(id, inventoryService);
    

    A few points: Use a domain service rather than passing in the other repository, because it better communicates what is going on. The domain service serves as a description of the contract that the aggregate actually needs. Furthermore, by refusing to pass the repository, you exclude any possibility that the order aggregate attempts to write to the item repository.

    (The trivial implementation of this domain service is to just forward the call to the repository, but the order aggregate doesn't need to know that).

    The domain service, in this case, should not be "helping" by choosing an action based on the availability of the inventory -- maybe the aggregate should throw, maybe the aggregate should throw for low volume orders/low priority purchasers, but use different rules when the order is over a million dollars. It's the order's job to figure that out, the domain service just provides the data.

    Given the data race, some false positives can slip through; detection and mitigation is a good idea.

    If you aren't willing to accept the data race (are you sure? Amazon accepts order for out of stock items all the time...), then you need to rethink the design of your model, and where you have set your aggregate boundaries.

    The null design of a model is to capture all of the business state into a single aggregate; its own state is internally consistent, but it might not be consistent with external state. When you start carving up the model into separate aggregates, you are making the same assertion -- the aggregate needs to be internally consistent, but it might not be consistent with state outside the aggregate.

    If that's not acceptable, then you turn Mother's picture to the wall, and implement your business rules directly in the book of record (ie, constraints in your RDBMS).