spring-boothibernatejpaaggregatedomain-driven-design

How to avoid N+1 or memory inefficiency when applying DDD Aggregate pattern?


I’m applying the DDD Aggregate pattern in a Spring Boot application. For example, Order is the aggregate root of OrderItem, and they are mapped like this:

@Entity
public class Order {

    @Id
    private Long id;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> orderItems = new ArrayList<>();

    public void renameOrderItem(Long itemId, String newName) {
        orderItems.stream()
            .filter(item -> item.getId().equals(itemId))
            .findFirst()
            .orElseThrow()
            .rename(newName);
    }
}

@Entity
public class OrderItem {

    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Order order;

    private String name;

    public void rename(String newName) {
        this.name = newName;
    }
}

✅ Problem

According to the DDD Aggregate rules, OrderItem must be modified through its root, Order.

So even for updating just one OrderItem, I need to load the entire Order and call order.renameOrderItem(...).

However, this leads to a few issues:

  1. Without fetch join → Accessing orderItems causes an N+1 problem.
  2. With fetch join → All OrderItems are loaded, even if I only need one → memory inefficiency.
  3. With orphanRemoval=true → If I only partially load OrderItems and then save the Order, the other items might get deleted.

❓ My Question

How can I safely modify an OrderItem through its aggregate root Order while still avoiding N+1 queries and memory waste when using Spring JPA and OneToMany mapping?

💡 What I’ve considered

•   Keep OrderItemEntity and domain model OrderItem separate
•   Load only the OrderItemEntity I need
•   Construct an Order domain model containing just this OrderItem
•   Run business logic via Order.findItem(id).rename()
•   Convert the updated domain model back to entity and save

This way I preserve aggregate integrity, but it results in additional conversion logic and a more complex repository layer.

❓ TL;DR

How do you respect aggregate encapsulation in DDD while also avoiding fetch-join-all or N+1 problems with JPA?

I’m especially interested in real-world solutions or architecture patterns that you’ve used in production.


I’ve seen several related questions, but none of them provided a clear answer.


Solution

  • Please be aware, that DDD only mandates that the domain layer invocations operate on the aggregate root level. Is does not mandate the same for the persistence layer invocations. That’s out of scope. That’s why I would go for the following approach assuming that you have an "OrderAggregateDomainService" with a method renameOrderItem(orderId, orderItemId, newOrderItemName). The method would basically perform the following 3 steps:

    1. Load the whole Order domain object via the “OrderRepository.retrieveOrderById(orderId)” call.

    2. Invoke Order.renameOrderItem(orderItemId, newOrderItemName) => I assume there is some business logic required that checks the validity of the “newOrderItemName” and that we want to implement that logic in the domain type.

    3. If the validity check is ok, then invoke a second repository method “OrderRepository.renameOrderItem(orderId, orderItem : OrderItem)” => This method does not load the whole order again, it only doublechecks if the persisted OrderItem actually refers to the given orderId and then performs the update of the OrderItem

    Additional Remarks:

    If someone else would try to rename the same OrderItem at the same time then we could actually face a race condition. Let’s assume both try to update from the same version 1. There are two cases: If you map the version attribute from the JPA entity also to the domain entity and vice versa, then the slower one should get the classical OptimisticLockException. If you do NOT map the version attribute to the domain entity and back, then things get too complicated to explain all variations in detail here, but the bottom line is that you should in fact map the version attribute to the domain entity.