2

I'm reading the DDD blue book and there is one thing that's pretty unclear to me as of yet and that is how to efficiently perform simple operations on Aggregates that have a lot of child Entities in them.

An example

  • Considering I have an Order, (Aggregate Root) that has Line Items.
  • Each Order can only have 1000 Line Items. (Business Rule).

Steps

In order for me to add a Line Item to the Order I'd have to:

  • Reconstitute the Order with all it's Line Items from it's repository.
    • orderRepo.getById(1)
  • Add the Line Item to the Order,
    • order.addLineItem(lineItem)
  • Save the Order via the repository again.
    • orderRepo.save(order)

Unless I'm missing something, this strikes me as terribly innefficient. What if I have 900 Line Items on that Order? I'd have to load a lot of data from the DB to perform this simple operation.

Yes, I could lazy load the LineItems but even then I won't be able to check the business rule that requires that <1000 Line Items must exist in an Order, unless all the Line Items are loaded.

Also, lazy loading implementations strike me as an anti-pattern because they usually tie the property accessors with repositories.

2

When using DDD, you compromise performance for better domain model. And this is an area where that compromise might be made.

Looking at your problem, I would question of LineItem should be part of Order aggregate. The point of aggregate is to group entities that multiple, complex operations are made on. Imagine you have an aggregate with internal entity and 10 operations. In one case, 9 of the operations work on the entities. In such a case, it would drastically complicate the situation if the entities weren't always loaded. If on the other hand only 1 of the operations need those entities, then I would question if those entities should be part of the aggregate. In case of complexity of the operations, your case is relatively simple. Taking count of items is trivial operation, that can be done on DB side. But imagine if the limit was for price and not for count. In this case, you have to run complex "price calculation" logic, which will probably require all order items with all the data. Meaning, you cannot afford not loading line items.

2

I believe your problem is you thinking you need to hold actual Item objects in your Order aggregate. That is wrong. If you were to do this, the memory footprint would really be insane and performance would be awful. And you should not do it, because a single operation (a command) should only affect one aggregate. This makes your code more cohesive and concise at the same time.

To stay much more performant and consistent, in reality the LineItem is a very simple object holding just references to identifiers of both Order and Item.

final class LineItem {

    private final OrderId orderId;

    private final ItemId itemId;

    public LineItem(OrderId orderId, ItemId itemId) {
        this.orderId = orderId;
        this.itemId  = itemId;
    }
}

and adding an Item to your Order aggregate would be modeled as follows:

public class Order {

    private static final int MAX_CAPACITY = 1000;

    private final OrderId orderId;

    private final List<LineItem> lineItems = new ArrayList<>();

    public void addItem(Item item) {
        if (lineItems.size() == MAX_CAPACITY) {
            // throw exception
        }

        lineItems.add(new LineItem(orderId, item.itemId()));
    }
}

If you are wondering how you're going to read data from this the answer is you do not have to and usually do not. Aggregates can grow in size but that's in favor of keeping your business rules consistent and visible. For reads and performance you will need a different layer talking to a database directly, completely circumventing the aggregates. You can do this because you know that all data in the database HAS to be valid, because all modifications are made through aggregates protecting the rules.

Another option to solving this issue is having the GetItemsCount(OrderId) in OrderRepository, as others have already suggested. Still, I believe your main problem is thinking about referencing aggregates incorrectly.

If you're looking at a very good source for aggregate modeling, definitely look at Effective Aggregate Design by Mr. Vaugh Vernon.

1
  1. Add a method to your repository called GetOrderItemsCount(), pass it the ID of the order you are interested in, and return the number of items currently in the order. This will satisfy your business rule, and you can optimize it any way you want.

  2. You don't have to load all of the existing items in an order to add a new one, especially if you already know the current count and have checked it against your business rule.

  3. Lazy loading? What lazy loading?

1

In DDD you need to load the entire Aggregate from the repository before you apply a command to it. If you think that you don't need to load it entirely then your Aggregate boundary may be wrong, you could have an Aggregate that is too big. Remember, Aggregates are the transactional boundary: every thing that happens inside an Aggregate is safe from concurrent updates, the operations are strong consistent. This safe-ness is nice but costly and thus must be done according to the real-world business.

Also, lazy loading implementations strike me as an anti-pattern because they usually tie the property accessors with repositories.

Yes, in the Aggregate design context, Lazy-loading is an anti-pattern. It means that your Aggregate boundary is wrong (most probable).

Each Order can only have 1000 Line Items. (Business Rule).

That is the maximum but you need to also consider the average case where an order have an average line item count of what, 5?

Yes, for a large order adding a line item will be slower but how often does it happen?

Also, you cannot protect this business rule by querying the repository for the line item count and check against the number 1000! This query+checking operation is eventual consistent (two line item could be added at the same time when there are 999 items in the order)!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.