Is it valid for two small aggregates to have the same root in DDD?

I have a data model with the following relationships: Shipment 1 --- * ShipmentLine 1 --- * Component I have two use cases around which I am trying to create a domain model. The use cases are: Shipping a line: A ShipmentLine is created on a Shipment by calling shipment.ShipLine(OrderLine line) (where OrderLine is a separate aggregate). There are three invariants around this: 1) No duplicate lines, 2) the Shipment must not be in a "Closed" state, and 3) the OrderLine's Vendor property must match the Shipment's Vendor property. All that I really need to enforce these invariants is an aggregate that looks something like the following: Shipment { Closed: bool Lines: LineId[] Vendor: string ShipLine(OrderLine line) } It seems to me that loading up all of the ShipmentLine 1 --> * Component relations just for this operation is a complete waste of resources and will lead to a confusing domain model, especially when those relations are not needed at all to enforce any invariants. Receiving a line: At a certain point the user will need to mark a line as "received". We track a "received quantity", and the user may receive a line multiple times. Without going into excessive detail, we do need the ShipmentLine 1 --> * Component relations loaded to enforce invariants for this operation. My aggregate needs to be something like this: Shipment { Closed: bool Lines: ShipmentLine[] ReceiveLine(ShipmentLine line, int qty) } ShipmentLine { Components: Component[] Receive(int qty) } Of course, there are many more operations which can be performed on this aggregate which I haven't listed here, and which do rely on all the relations being loaded. It seems to me as if I almost have two different aggregates here: perhaps a ShipLineRequest aggregate (terrible name, I know) and a Shipment aggregate. Everything I've read says to generally keep aggregates small, containing only the data they need to enforce their invariants, and two separate aggregates accomplishes that. However, it bothers me that I now have two aggregates which contain the same root with the same global ID, and one of which only contains one behavior. Taken to extremes, it feels like I'd end up writing an entirely new aggregate for every single behavior. However, there are many more properties and relationships than I've detailed here on my larger aggregate, and it feels like a violation of the "only what you need" principle to load more and more properties and relations into the same aggregate just because they are needed by one behavior. Additionally, I feel I'm falling into the trap of essentially just mirroring the data model. So, my question is: is it better practice to model small, concise aggregates which contain only the data they need for their invariants but end up "duplicating" the aggregate root, or to model one large aggregate which may load a bunch of data which is not needed for half of its behaviors/invariants?

Mar 17, 2025 - 17:56
 0
Is it valid for two small aggregates to have the same root in DDD?

I have a data model with the following relationships:

Shipment 1 --- * ShipmentLine 1 --- * Component

I have two use cases around which I am trying to create a domain model. The use cases are:

  1. Shipping a line: A ShipmentLine is created on a Shipment by calling shipment.ShipLine(OrderLine line) (where OrderLine is a separate aggregate). There are three invariants around this: 1) No duplicate lines, 2) the Shipment must not be in a "Closed" state, and 3) the OrderLine's Vendor property must match the Shipment's Vendor property. All that I really need to enforce these invariants is an aggregate that looks something like the following:

    Shipment {
      Closed: bool
      Lines: LineId[]
      Vendor: string
      ShipLine(OrderLine line)
    }
    

    It seems to me that loading up all of the ShipmentLine 1 --> * Component relations just for this operation is a complete waste of resources and will lead to a confusing domain model, especially when those relations are not needed at all to enforce any invariants.

  2. Receiving a line: At a certain point the user will need to mark a line as "received". We track a "received quantity", and the user may receive a line multiple times. Without going into excessive detail, we do need the ShipmentLine 1 --> * Component relations loaded to enforce invariants for this operation. My aggregate needs to be something like this:

    Shipment {
      Closed: bool
      Lines: ShipmentLine[]
      ReceiveLine(ShipmentLine line, int qty)
    }
    
    ShipmentLine {
      Components: Component[]
      Receive(int qty)
    }
    

    Of course, there are many more operations which can be performed on this aggregate which I haven't listed here, and which do rely on all the relations being loaded.

It seems to me as if I almost have two different aggregates here: perhaps a ShipLineRequest aggregate (terrible name, I know) and a Shipment aggregate. Everything I've read says to generally keep aggregates small, containing only the data they need to enforce their invariants, and two separate aggregates accomplishes that.

However, it bothers me that I now have two aggregates which contain the same root with the same global ID, and one of which only contains one behavior. Taken to extremes, it feels like I'd end up writing an entirely new aggregate for every single behavior. However, there are many more properties and relationships than I've detailed here on my larger aggregate, and it feels like a violation of the "only what you need" principle to load more and more properties and relations into the same aggregate just because they are needed by one behavior. Additionally, I feel I'm falling into the trap of essentially just mirroring the data model.

So, my question is: is it better practice to model small, concise aggregates which contain only the data they need for their invariants but end up "duplicating" the aggregate root, or to model one large aggregate which may load a bunch of data which is not needed for half of its behaviors/invariants?