Loading...
Skip to Content

Aggregate Best Practices

 This article explains how to apply Domain-Driven Design (DDD) principles to solve common problems when using JPA/Hibernate. It introduces several issues you may run into with JPA and Hibernate and presents an approach to resolving them.

You may run into the following problems when using JPA/Hibernate.

  • Loading the entire database into memory due to unintended eager loading
  • The N+1 lazy loading problem
  • Unintended eager ToOne mappings
  • LazyInitializationException
  • Unintended cascading deletes or orphan records
  • Performance degradation due to unintended eager loading

Most of these problems stem from inconsistent decisions made at the design stage. This training material shows how to apply Domain-Driven Design principles to solve them.

 Relational database schema design

To apply Domain-Driven Design, you first need to define the business subdomains. Then you can define the aggregates/entities to store in the relational database.
The key point is not to start with the Hibernate entity design, but to create an entity-relationship diagram (ERD) first.

For example, suppose we have the following entities.

  • Customer
  • Customer Home Address
  • Order
  • Order Line (quantity of each product)
  • Shipping Address
  • Product
  • Product Category

The ERD showing the basic relationships among them looks like this.

Relational database schema design

 Finding the aggregate roots

In Domain-Driven Design, an aggregate root is "the entity that acts as the gateway to the whole aggregate and is responsible for maintaining the aggregate's integrity and consistency." Put simply, it means finding the most important and relevant primary entities in the service.

For example, suppose we have the following requirements.

  1. Customers must be easy to look up by email address
  2. A customer's home address must be easy to retrieve
  3. A given customer's orders must be easy to retrieve
  4. A given order's details must be easy to retrieve (including the various products in the order and the quantity of each)

Based on these requirements, Customer, Order, and Product can be grouped as aggregate roots.

Heuristic 1: Spring Repositories should be defined only for aggregate root entities.

Therefore, only CustomerRepository, OrderRepository, and ProductRepository should be defined.

Finding the aggregate roots

 Four decisions when mapping Hibernate relationships

When creating a Hibernate relationship mapping, you need to make the following four decisions.

  1. Whether to create a relationship mapping at all
  2. Unidirectional or bidirectional relationship
  3. Lazy or eager loading
  4. Cascade settings

 Four decisions when mapping Hibernate relationships
 1. Whether to create a relationship mapping

Heuristic 2: Within an aggregate, consider using Value objects instead of @OneToOne or some @ManyToOne mappings.

For example, HomeAddress can be treated as a Value object rather than an entity. In JPA, this can be implemented with the @Embeddable and @Embedded annotations.

@Embeddable public class Address { private String street; private String city; private String zipCode; // getters and setters } @Entity public class Customer { @Id private Long id; @Embedded private Address homeAddress; // other fields, getters and setters }

Heuristic 3: Avoid relationship mappings between aggregate roots; map the foreign key as-is instead.

For example, the relationship from the Order entity to Customer can be mapped as follows.

@Entity public class Order { @Id private Long id; private Long customerId; // use the foreign key as-is // other fields, getters and setters }

Heuristic 4: Allow relationship mappings to other aggregate roots only when the relationship is finite.

For example, the relationship between Product and ProductCategory is finite, so it can be mapped as follows.

@Entity public class Product { @Id private Long id; @ManyToOne @JoinColumn(name = "category_id") private ProductCategory category; // other fields, getters and setters }

 Four decisions when mapping Hibernate relationships
 2. Unidirectional or bidirectional relationship

Heuristic 5: Always start with unidirectional relationships. Convert to bidirectional only when the need arises and the relationship does not cross an aggregate root.

For example, the relationship between Order and OrderLine can start out unidirectional, as follows.

@Entity public class Order { @Id private Long id; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) private List<OrderLine> orderLines = new ArrayList<>(); // other fields, getters and setters } @Entity public class OrderLine { @Id private Long id; private int quantity; // other fields, getters and setters }

 Four decisions when mapping Hibernate relationships
 3. Lazy or eager loading

Heuristic 6: Start with lazy loading for all child relationships from the aggregate root. Convert to eager loading only when the related entity is always needed.

@Entity public class Order { @Id private Long id; @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private List<OrderLine> orderLines = new ArrayList<>(); // other fields, getters and setters }

Heuristic 7: Never use eager loading for relationships to other aggregate roots.

@Entity public class Product { @Id private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id") private ProductCategory category; // other fields, getters and setters }

 Four decisions when mapping Hibernate relationships
 4. Cascade settings

Heuristic 8: Configure cascade operations for every entity that is not an aggregate root. Do not configure cascades for associated aggregate entities.

@Entity public class Order { @Id private Long id; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) private List<OrderLine> orderLines = new ArrayList<>(); private Long customerId; // no cascade setting for other aggregate roots // other fields, getters and setters }

 Conclusion

This training material explained how to apply Domain-Driven Design concepts to JPA/Hibernate relationship design. Here is a summary of the eight heuristics.

  1. Spring Repositories should be defined only for aggregate root entities.
  2. Within an aggregate, consider using Value objects instead of @OneToOne or some @ManyToOne mappings.
  3. Avoid relationship mappings between aggregate roots; map the foreign key as-is instead.
  4. Allow relationship mappings to other aggregate roots only when the relationship is finite.
  5. Always start with unidirectional relationships. Convert to bidirectional only when the need arises and the relationship does not cross an aggregate root.
  6. Start with lazy loading for every child relationship on an aggregate root. Switch to eager loading only when the related entity is always needed.
  7. Never use eager loading for a relationship to another aggregate root.
  8. Configure cascade operations for every entity that is not an aggregate root. Do not configure cascades for associated aggregate entities.

Following these heuristics prevents many of the problems that arise when using JPA/Hibernate and leads to a more efficient database design.

*Translator's note: lazy loading is acceptable within the same bounded context.