Can aggregate root reference another root?
Asked Answered
R

3

25

I'm a little bit confused. I just watched
Julie Lerman's Pluralsight video on DDD and here's the confusion I have: Having a simple online store example with: Purchase Orders with Items for Suppliers, what's the aggregate root here?

Technically Purchase Order, right? It's for particular Supplier and has items on it. That makes sense.

But.. is the Item an aggregate root as well? It has other "sub-objects" like "Brand", "Designer", "Color", "Type" etc... You might have a separate application in your SOA system to Edit and Manage items (without PO). So..in that case you will have to access a component of aggregate root - which is not allowed.

Is the Item aggregate root in this example or not?

Repugnant answered 30/5, 2018 at 5:49 Comment(11)
I don't know or watched that Pluralsight video, but that would be considered as a different bounded context. So in its own bounded context then yah you could say it's and aggregate root.Blockbusting
dont get too stucked on what you watched in a video tutorialDupre
In DDD you can reference an aggregate root from another aggregate. What you cannot do is reference anything inside the other aggregate root. II think that your aggregate roots are probably PurchaseOrder, Supplier and StockItem. Don't get confised between a PurchaseOrderLine and a StockItem. A PurchaseOrderLine belongs to the PurchaseOrder aggregate and will reference a StockItem.Freiburg
@Mark so.. correct me if I'm wrong, but I assume I will need inject data persistence object into my aggregate root. For example: In StockItem object I have GetOnHand method. This method must go to the database. Is it a good practice to initialize a domain Entity (aggregate root) with an implementation of IItemData interface that has GetOnHand method?Repugnant
@IshThomas no, AR1 doesn't have to (and, generally speaking, shouldn't) fetch AR2 from the database. It is also not recommended that AR1 base its decisions on data contained in AR2. Most AR1=>AR2 references are going to be by ID and are only here to help Application Services, or more rarely Domain Services, fetch a related aggregate by following a "pointer" from an aggregate they already know.Lordosis
@Lordosis What I mean is how AR in general fetch data? It has to have some interface *Data. So that I can inject data access via constructor, right?Repugnant
how AR in general fetch data? Simple answer - it doesn't. It already has all the data it needs the moment it begins to exist in memory.Lordosis
@Lordosis oh.. then, what initializes the data in ARs?Repugnant
The repository. Or the consumer that instantiates the AR, in the case of a new AR.Lordosis
@Lordosis so.. maybe this time I understood it correctly: the Service layer (aka Logic/Business/Manager) will have Data (data access interface). Data will be injected into Service layer. Data returns ARs (filled with data and ready to use by a Service). The Service does the logic and returns DTO objects wherever... Am I correct now?Repugnant
Something along these lines, yes. It surprises me that the Pluralsight course doesn't explain it. Maybe later than where you're at in the video?Lordosis
L
43

This is dependent on the context you are in. I will try to explain with a few different context examples and answer the question at the end.

Let's say the first context is all about adding new items to the system. In this context the Item is the aggregate root. You will most likely be constructing and adding new items to your data store or remove items. Let's say the class might look as follows:

namespace ItemManagement
{
    public class Item : IAggregateRoot // For clarity
    {
        public int ItemId {get; private set;}

        public string Description {get; private set;}

        public decimal Price {get; private set;}

        public Color Color {get; private set;}

        public Brand Brand {get; private set;} // In this context, Brand is an entity and not a root

        public void ChangeColor(Color newColor){//...}

        // More logic relevant to the management of Items.
    }
}

Now let's say a different part of the system allows the composition of a purchase order by adding and removing items from the order. Not only is Item not an aggregate root in this context, but ideally it will not even be the same class. Why? Because Brand, Color and all of the logic will most likely be completely irrelevant in this context. Here is some example code:

namespace Sales
{
    public class PurchaseOrder : IAggregateRoot
    {
        public int PurchaseOrderId {get; private set;}

        public IList<int> Items {get; private set;} //Item ids

        public void RemoveItem(int itemIdToRemove)
        {
            // Remove by id
        }

        public void AddItem(int itemId) // Received from UI for example
        {
            // Add id to set
        }
    }
}

In this context Item is only represented by an Id. This is the only relevant part in this context. We need to know what items are on the purchase order. We don't care about brand or anything else. Now you are probably wondering how would you know the price and description of items on the purchase order? This is yet another context - view and removing items, similar to many 'checkout' systems on the web. In this context we might have the following classes:

namespace Checkout
{
    public class Item : IEntity
    {
        public int ItemId {get; private set;}

        public string Description {get; private set;}

        public decimal Price {get; private set;}
    }

    public class PurchaseOrder : IAggregateRoot
    {
        public int PurchaseOrderId {get; private set;}

        public IList<Item> Items {get; private set;}

        public decimal TotalCost => this.Items.Sum(i => i.Price);

        public void RemoveItem(int itemId)
        {
            // Remove item by id
        }
    }
}

In this context we have a very skinny version of item, because this context does not allow alteration of Items. It only allows the viewing of a purchase order and the option to remove items. The user might select an Item to view, in which case the context switches again and you may load the full item as the aggregate root in order to display all the relevant information.

In the case of determining whether you have stock, I would think that this is yet another context with a different root. For example:

namespace warehousing
{
    public class Warehouse : IAggregateRoot
    {
        // Id, name, etc

        public IDictionary<int, int> ItemStock {get; private set;} // First int is item Id, second int is stock

        public bool IsInStock(int itemId)
        {
            // Check dictionary to see if stock is greater than zero
        }
    }
}

Each context, through its own version of the root and entities, exposes the information and logic it requires to perform its duties. Nothing more and nothing less.

I understand that your actual application will be significantly more complex, requiring stock checks before adding items to a PO, etc. The point is that your root should ideally already have everything loaded that is required for the function to be completed and no other context should affect the setup of the root in a different context.

So to answer your question - Any class could be either an entity or a root depending on the context and if you've managed your bounded contexts well, your roots will rarely have to reference each other. You don't HAVE to reuse the same class in all contexts. In fact, using the same class often leads to things like a User class being 3000 lines long because it has logic to manage bank accounts, addresses, profile details, friends, beneficiaries, investments, etc. None of these things belong together.

To respond to your questions

  1. Q: Why Item AR is called ItemManagement but PO AR is called just PurchaseOrder?

The namespace name reflects the name of the context you are in. So in the context of item management, the Item is the root and it is placed in the ItemManagement namespace. You can also think of ItemManagement as the Aggregate and Item as the Root of this aggregate. I'm not sure if this answers your question.

  1. Q: Should Entities (like light Item) have methods and logic as well?

That entirely depends on what you context is about. If you are going to use Item only for displaying prices and names, then no. Logic should not be exposed if it should not be used in the context. In the Checkout context example, the Item has no logic because they only serve the purpose of showing the user what the purchase order is composed of. If there is a different feature where, for example, the user can change the color of an item (like a phone) on the purchase order during checkout, you might consider adding this type of logic on the item in that context.

  1. How ARs access database? Should they have an interface.. let's say IPurchaseOrderData, with a method like void RemoveItem(int itemId)?

I apologize. I assumed that your system is using some sort of ORM like (N)Hibernate or Entity framework. In the case of such an ORM, the ORM would be smart enough to automatically convert collection updates to the correct sql when the root is persisted (given that your mapping is configured correctly). In the case where you manage your own persistence, it's slightly more complicated. To answer the question directly - you can inject a datastore interface into the root, but I would suggest rather not.

You could have a repository that can load and save aggregates. Lets take the purchase order example with items in the CheckOut context. Your repository will likely have something like the following:

public class PurchaseOrderRepository
{
    // ...
    public void Save(PurchaseOrder toSave)
    {
        var queryBuilder = new StringBuilder();

        foreach(var item in toSave.Items)
        {
           // Insert, update or remove the item
           // Build up your db command here for example:
           queryBuilder.AppendLine($"INSERT INTO [PurchaseOrder_Item] VALUES ([{toSave.PurchaseOrderId}], [{item.ItemId}])");

        }
    }
    // ...
}

And your API or service layer would look like something this:

public void RemoveItem(int purchaseOrderId, int itemId)
{
    using(var unitOfWork = this.purchaseOrderRepository.BeginUnitOfWork())
    {
        var purchaseOrder = this.purchaseOrderRepository.LoadById(purchaseOrderId);

        purchaseOrder.RemoveItem(itemId);

        this.purchaseOrderRepository.Save(purchaseOrder); 

        unitOfWork.Commit();
    }
}

In this case your repository could become quite hard to implement. It might actually be easier to make it delete items on the purchase order and re-add the ones that are on the PurchaseOrder root (easy but not recommended). You would have a repository per aggregate root.

Off topic: An ORM like (N)Hibernate will deal with the Save(PO) by tracking all changes made to your root since it was loaded. So it will have an internal history of what has changed and issue the appropriate commands to bring your database state in sync with your root state when you save by emitting SQL to address each change made to the root and its children.

Looney answered 31/5, 2018 at 7:4 Comment(3)
Thank you for your answer. I have a few questions: 1. Why Item AR is called ItemManagement but PO AR is called just PurchaseOrder? 2. Should Entities (like light Item) have methods and logic as well? 3. How ARs access database? Should they have an interface.. let's say IPurchaseOrderData, with a method like void RemoveItem(int itemId)? They will still have no knowledge about the data source. I will inject that knowledge via interface. So the constructor of PurchaserOrder AR would be public class PurchaseOrder (IPurchaseOrderData poData) { ... } Would you agree?Repugnant
In case we have one DB for all bounded contexts then we probably have some duplicates eg. "Full" Item and "Light" Item are the same entity but using different versions of the Item we end up with more than one table of the same item. Is this right decision in case we don't have one DB per bounded context?Stabilizer
@Stabilizer I would not duplicate the tables. One rule of DDD is that your domain is separate from your data. Completely. You can map the same table to different domain entities. So you have one table with 10 fields, but in context A you map 1,3 and 5, but in context B you can map 1,4 and 7 for example. Unless it makes sense for your data to be separate.Looney
A
19

Although this question has an accepted answer, reading this article may help the other readers of this question.
According to this section of the article, Instead of referencing another aggregate directly, create a value object that wraps the ID of the aggregate root and use that as the reference. This makes it easier to maintain aggregate consistency boundaries since you cannot even accidentally change the state of one aggregate from within another. It also prevents deep object trees from being retrieved from the data store when an aggregate is retrieved.

Avlona answered 18/4, 2020 at 7:53 Comment(0)
B
2

Having a simple online store example with: Purchase Orders with Items for Suppliers, what's the aggregate root here?

That depends on how you model it, which in turn should depend on how you think the information changes over time.

For instance, one possible model would be to put all of those entities into a single aggregate.

More common would be to treat each purchase order separately from the others; in that case, you would probably make each order an aggregate root. Since several orders are likely to have relations to the same supplier, the supplier is probably an aggregate as well.

Items is less clear - entries into the order are probably local to that order, so it is less likely that you would create a separate consistency boundary to manage them. On the other hand, products/skus are likely to be re-used by multiple orders, which again suggests that they are a separate aggregate.

What usually happens in this case, is that the aggregates don't contain references to each other, but keys that can be used to look up references.

So my purchase order (#12345) might include "2 units of product (#67890)", but if I want to know what that means, then I have to take the product (#67890) and use that to look up the rest of the data for the product.

If I want to have some PO's logic like "Do something with items that we have in stock" I would have to get all items on that PO and call IsInStock() method on them. IsInStock is a public method of the Item so I guess I'm not violating DDD principles. Am I?

Short answer: no.

Longer answer: where you want to be very careful is when you have two pieces of data that must be in agreement at all times. Trying to coordinate the semantics of data in different aggregates gets really messy.

Braze answered 30/5, 2018 at 13:42 Comment(2)
Thanks for that clarification. So.. for example: If I want to have some PO's logic like "Do something with items that we have in stock" I would have to get all items on that PO and call IsInStock() method on them. IsInStock is a public method of the Item so I guess I'm not violating DDD principles. Am I?Repugnant
Apologies if this is redundant. If I want to send a representation of the PO to the client where each product in the PO contains meaningful properties like productName, would it make sense to have the app service load in the PO AR and the Product ARs and then map them to a DTO?Aspersion

© 2022 - 2024 — McMap. All rights reserved.