How to Persist DDD Aggregates with JPA, Hibernate, and MongoDB

This tutorial walks through persisting Domain‑Driven Design aggregates by first explaining aggregates and aggregate roots, then showing Java class examples, followed by practical JPA/Hibernate integration challenges and a complete MongoDB document‑store solution, highlighting trade‑offs and best practices.

Programmer DD
Programmer DD
Programmer DD
How to Persist DDD Aggregates with JPA, Hibernate, and MongoDB

1. Overview

In this tutorial we explore the possibilities of persisting DDD aggregates using different technologies.

2. Introduction to Aggregates

An aggregate is a group of business objects that must always remain consistent, so we save and update the aggregate as a whole within a transaction.

2.1 Purchase Order Example

class Order {
    private Collection<OrderLine> orderLines;
    private Money totalCost;
    // ...
}

class OrderLine {
    private Product product;
    private int quantity;
    // ...
}

class Product {
    private Money price;
    // ...
}

These classes form a simple aggregate. The orderLines and totalCost fields must always be consistent; totalCost should equal the sum of all orderLines .

Adding plain getters and setters can break encapsulation and violate business constraints.

2.2 Aggregate Design

If we add getters and setters for all properties (including setOrderTotal), we can set totalCost to zero, which violates the rule that the total cost must never be zero dollars.

2.3 Aggregate Root

The aggregate root is the entry point class for the aggregate. All business operations should go through the root, ensuring the aggregate stays in a consistent state.

In our example, Order is the proper aggregate root. We modify it to enforce invariants:

class Order {
    private final List<OrderLine> orderLines;
    private Money totalCost;

    Order(List<OrderLine> orderLines) {
        checkNotNull(orderLines);
        if (orderLines.isEmpty()) {
            throw new IllegalArgumentException("Order must have at least one order line item");
        }
        this.orderLines = new ArrayList<>(orderLines);
        totalCost = calculateTotalCost();
    }

    void addLineItem(OrderLine orderLine) {
        checkNotNull(orderLine);
        orderLines.add(orderLine);
        totalCost = totalCost.plus(orderLine.cost());
    }

    void removeLineItem(int line) {
        OrderLine removedLine = orderLines.remove(line);
        totalCost = totalCost.minus(removedLine.cost());
    }

    Money totalCost() {
        return totalCost;
    }
}

This design keeps the aggregate immutable from the outside while allowing controlled modifications through methods that maintain business invariants.

3. JPA and Hibernate

We try to persist the order aggregate with JPA and Hibernate using Spring Boot and the JPA starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

A test case demonstrates the problem:

@DisplayName("given order with two line items, when persist, then order is saved")
@Test
public void test() throws Exception {
    // given
    JpaOrder order = prepareTestOrderWithTwoLineItems();
    // when
    JpaOrder savedOrder = repository.save(order);
    // then
    JpaOrder foundOrder = repository.findById(savedOrder.getId()).get();
    assertThat(foundOrder.getOrderLines()).hasSize(2);
}

The test throws

java.lang.IllegalArgumentException: Unknown entity: com.baeldung.ddd.order.Order

because several JPA requirements are missing:

Add mapping annotations.

Make OrderLine and Product entities or @Embeddable classes.

Provide a no‑args constructor for each entity/embeddable.

Replace the Money type with a simple type.

3.1 Changes to Value Objects

Persisting the aggregate forces us to break the value‑object design: fields can no longer be final, and we need artificial IDs even though the classes were never intended to have identifiers.

3.2 Complex Types

JPA cannot automatically map third‑party complex types such as Joda‑Money. We could write a custom @Converter (JPA 2.1) or split the money into separate basic fields (currency code, numeric code, decimal places, amount).

3.3 Conclusion

While JPA is the most widely adopted specification, it may not be the best choice for persisting order aggregates because it forces compromises on the domain model.

4. Document Store

Document stores keep the whole object together, making them an ideal candidate for persisting aggregates.

4.1 Persisting the Aggregate with MongoDB

Add the MongoDB starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

A similar test works with a Mongo repository:

@DisplayName("given order with two line items, when persist using mongo repository, then order is saved")
@Test
public void test() throws Exception {
    // given
    Order order = prepareTestOrderWithTwoLineItems();
    // when
    repo.save(order);
    // then
    List<Order> foundOrders = repo.findAll();
    assertThat(foundOrders).hasSize(1);
    List<OrderLine> foundOrderLines = foundOrders.iterator().next().getOrderLines();
    assertThat(foundOrderLines).hasSize(2);
    assertThat(foundOrderLines).containsOnlyElementsOf(order.getOrderLines());
}

The resulting BSON document stores the entire aggregate as a single JSON‑like structure, preserving complex objects such as Joda‑Money without needing custom converters:

{
    "_id": ObjectId("5bd8535c81c04529f54acd14"),
    "orderLines": [
        {
            "product": {
                "price": {
                    "money": {
                        "currency": {"code": "USD", "numericCode": 840, "decimalPlaces": 2},
                        "amount": "10.00"
                    }
                }
            },
            "quantity": 2
        },
        {
            "product": {
                "price": {
                    "money": {
                        "currency": {"code": "USD", "numericCode": 840, "decimalPlaces": 2},
                        "amount": "5.00"
                    }
                }
            },
            "quantity": 10
        }
    ],
    "totalCost": {
        "money": {
            "currency": {"code": "USD", "numericCode": 840, "decimalPlaces": 2},
            "amount": "70.00"
        }
    },
    "_class": "com.baeldung.ddd.order.mongo.Order"
}

Notice that we did not change the order of the original aggregate classes, nor did we need setters, default constructors, or custom converters for the money type.

4.2 Conclusion

Using MongoDB to persist aggregates is simpler than using JPA, though it does not imply MongoDB is universally superior; relational databases are still appropriate in many scenarios.

5. Final Conclusion

Aggregates in DDD are often the most complex part of a system and require a different approach than typical CRUD applications. Popular ORM solutions can oversimplify the domain model and hinder enforcement of business rules. Document stores provide a way to persist aggregates without sacrificing model richness.

Original link: https://www.baeldung.com/spring-persisting-ddd-aggregates
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootDDDMongoDBAggregatesHibernatejpa
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.