Awkward Aggregate Root relationships and how to rethink them

I have an Order that can be split into multiple Shipments. A Customer orders Quantity X of Product Y, represented as a single OrderLine with Quantity = X. In order to optimize shipping costs, Quantity X of Product Y is split into Z number of Shipments….

OrderLine needs to know how much Quantity has been partially shipped and what remains to-be-shipped, OrderLine.QtyShipped and OrderLine.QtyRemaining respectively. It has to maintain these counters with data from ShipmentLines…

ShipmentLines can easily be modeled as Value Objects, we just copy the OrderLines data to them. However the OrderLines can’t easily be Value Objects. If I want to keep my shipment quantity counters up to date, I need a reference to ShipmentLine from within the Aggregate Root: Order… isn’t this a DDD no-no? Shouldn’t I be favoring Value Objects over Entities? I know I shouldn’t be referencing child Entities from another Aggregate Root.

What if the Shipment needs to be canceled?… What happens to the OrderLines that the Shipment was made for? I need a way to update the status and quantities of the OrderLine that got canceled. How can I do any of this without OrderLine being an Entity with an ID that’s referenced inside of ShipmentLine?

There are forces pushing you to say “guess I really do have to hold that OrderLine reference inside my Shipment AR, or make OrderLine its own Aggregate Root, it all just feels weird…”

How would it be done without computers?

One good way to flesh stuff like this out is to think, and talk to domain experts, about how all of this would be done without computers.

Well, the Order would look a lot like a paper Invoice, OrderLines displayed in a table with fields like: Line Number, SKU, Product Name, Quantity, Price… Line Number would just be ordinal, not a meaningful ID.

Once the Order was filled out (I’m thinking one of those carbon copy deals), it would be handed to a Shipping Supervisor to be fulfilled. The Shipping Supervisor would create a Picklist of items that need to be packaged. This Picklist would then be handed to a Runner to locate and fetch the item(s).

The Runner will locate the Items on the Picklist using a paper Item Location Map. He/she picks up the Items and returns to the Shipping Supervisor. Once the Package is complete, the Shipping Supervisor creates a Packing Slip indicating Items in the Package. This contains the Order Number that the Shipment is for.

The Packing Slip has a table like the Order, containing fields like: Line Number, SKU, and Quantity. No Order Line identifier is needed on the Picklist or Packing Slip… the fact that the same Product is present in multiple OrderLines is irrelevant to the Shipping Supervisor.

Once the Package is ready, the Shipping Supervisor calls the Courier to come pick up the Package. He drops a carbon copy of the Packing Slip into the ‘To Be Picked Up’ pile. The Runner takes the Package to the Warehouse to be picked up by the Courier.

There’s always a chance that the Shipping Supervisor receives an updated version of the Order from the Storefront to replace the old one. Now what? Say the new Order has less Products than the first one, and some OrderLines that were in the first Order are missing in this new version. The shipping Supervisor simply looks over the existing Packing Slip and compares it to the new Order. He directs the Runner to un-pack the Package and put some of the Items back at their Warehouse Location.

Even up until this point, we haven’t surfaced any requirement that ShipmentLine must hold a reference to OrderLine. The Shipping Supervisor may scribble numbers on the Order or Picklist that tells his team how much of the Order has been shipped vs yet-to-be shipped, but he still doesn’t really care about what specific OrderLines he’s affecting - especially when there are multiple OrderLines with the same Product, or when he’s partially Shipping an OrderLine’s Quantity.

3rd party influence

After this deep exploration of a paper-based process, we can look back at our original problem: “I’m feeling like I have to hold a Child Entity to Child Entity relationship for this Use Case and I know that’s against the rules.” The paper-based version doesn’t have this requirement. So where is it coming from? Maybe it’s a requirement by some other Bounded Context that has leaked into your Shipping and Sales BC’s.

Maybe it turns out your team has committed to using Shopify as a Storefront. It’s Shopify that requires that every OrderLine, once it’s shipped, get a status update for the OrderLine back from the Shipping BC. You’ve separated an implementation detail from your Domain, and you can proceed to mitigate this boundary leakage somehow. Maybe you build up your defense against Shopify leaking into your Domain, emitting an Event that instructs the Storefront BC to maintain your Shipped/Remaining counters.

Summary

However you decide to mitigate this 3rd party boundary leakage; by going through this exercise of “How would it be done without computers?” you’ve gained deeper insight into the real business constraints imposed by your Domain. When we started, we had a a mixture of technical and business constraints muddying the issue at hand. By thinking about how humans would interact with each other to perform a task, you are able to think more purely about the Domain instead of getting too far ahead of yourself with implementation details.

You may also be able to use this technique to sort out what may or may not be consistency constraints. If the goal of the software project is to remove a specific bottleneck - why should you focus on consistency requirements for processes that aren’t bottlenecks? If a process is already eventually consistent without computers and isn’t a bottleneck, you have some good ammunition to argue against strong consistency in the software.

As always, make sure you click the big red button below to sign up for weekly link lists and content that will help you take your .NET development to the next level.

Tweet
comments powered by Disqus