Effective Domain-Driven Design for JPA and Hibernate Mappings
Written on
In this article, we will explore the best practices for implementing Domain-Driven Design (DDD) principles in JPA and Hibernate mappings, aimed at overcoming common challenges associated with JPA.
Welcome to the concluding part of our series on best practices for JPA and Hibernate. I will reference prior articles throughout this discussion, so feel free to explore them for deeper insights. You can also follow the guidelines provided here for effective implementation.
A Look at Database Query Techniques in Spring Data JPA
The Spring JPA framework simplifies interactions with relational databases by providing essential boilerplate code.
Understanding the N+1 Loading Problem in Hibernate/JPA
This article serves as the second part of our exploration into Hibernate/JPA within the Spring ecosystem. Before diving in, I encourage you to review previous discussions.
Transaction vs. JPA Session Boundaries in Spring JPA
This topic focuses on managing query execution effectively.
Comprehensive Guide to JPA/Hibernate Relationship Mappings
This guide discusses best practices and highlights the pitfalls of bidirectional relationships.
Background
Over the past two decades of working intermittently with JPA and Hibernate, I have encountered numerous anti-patterns and bugs. Anyone familiar with JPA/Hibernate has likely faced challenges such as:
- Loading entire databases into memory due to unintended eager fetching.
- The notorious N+1 lazy fetching issue.
- Accidental eager ToOne mappings.
- LazyInitializationException errors.
- Unintentional cascade deletes or orphaned records.
- Poor performance resulting from unexpected eager fetching.
These challenges have led some colleagues to develop a strong aversion to Hibernate and ORM frameworks, opting instead for more straightforward SQL templating solutions. However, I believe many of these complications arise from inconsistent design choices during the planning phase. While ORM frameworks come with their challenges, I firmly advocate for their use (especially Hibernate in a Java context) when we have comprehensive control over the underlying relational database schemas.
Although the official Spring JPA documentation does not delve deeply into adopting DDD principles, it is indeed recommended. This article aims to share practical heuristics for creating effective Hibernate relationships using DDD principles to mitigate the aforementioned issues.
Designing the Database Schema
To implement domain-driven design effectively, we must first identify the relevant business subdomain. From there, we can determine the aggregates/entities to store in our relational database. Here, the term "entities" refers specifically to JPA entities, not DDD entities.
The first step in establishing ideal Hibernate mappings for database entities is to ensure the schema design is correct. Before proceeding with this, create a component diagram that illustrates the relationships among your components.
“DO NOT start with Hibernate entity design. Create the entity relationship diagram (ERD) first, then proceed to ORM mappings.”
Assume we have the following entities:
Customer Customer Home Address Order Order Line: with count of each product Shipping Address Product Product Category Product
A basic pseudo ERD illustrating their relationships might look like this:
One key decision at this stage is whether to share a table for both ShippingAddress and HomeAddress. This would result in six tables: customer, order, address, orderline, product, and product_category.
Now updated with some OneToMany relationships:
Identifying Aggregate Roots
In DDD, an aggregate root is defined as “an entity that serves as a gateway to the entire aggregate, ensuring its integrity and consistency.” Simply put, you need to determine your primary and most significant entities for your service, which will depend on the business context.
Let’s assume we are building a monolithic service that requires the following:
- Easy retrieval of customers by email.
- Easy access to a customer’s home address.
- Quick access to orders placed by a specific customer.
- Retrieval of order details, including products and their quantities.
Based on these requirements, we identify Customer, Order, and Product as the Aggregate Roots. Consequently, all database queries should focus on these aggregate root entities. For instance, direct SQL queries against the Address table should be avoided; instead, access should be through Customer or Order.
Heuristic 1: Spring Repositories should only be defined for Aggregate Root Entities.
Once aggregate roots are identified, we can leverage this information to establish Hibernate mappings for each entity.
This implies that our application will only utilize CustomerRepository, OrderRepository, and ProductRepository. Other entities can only be referenced indirectly via these three Aggregate Roots.
Four Key Decisions for Hibernate Relationship Mapping
- To create relationship mappings or not.
- Choosing between unidirectional or bidirectional relationships.
- Deciding on lazy or eager fetching.
- Determining what to cascade.
Deciding on Relationship Mappings
One of the most significant anti-patterns in Hibernate mapping is creating a mapping for every relationship depicted in the ERD. Just because a relationship exists does not necessitate a corresponding Hibernate mapping; sometimes, a foreign key can simply be a standard attribute.
The best way to avoid poorly formed relationship mappings is to eliminate them altogether!
As mentioned in previous discussions, not all mappings need to be created. For example, HomeAddress might not need to be an entity; it can function as a Value Object. In JPA, this can be achieved using the @Embeddable and @Embedded annotations.
Heuristic 2: Within an Aggregate, consider using Value Objects instead of `@OneToOne` or `@ManyToOne`.
Another important consideration arises when an Aggregate Root is related to another. For instance, a Customer can have multiple Orders, but both are Aggregate Roots. While it might be tempting to create a mapping, doing so complicates management and can lead to performance issues when fetching data across Aggregate Roots. There have been instances where the entire database was loaded into memory due to eager fetching.
Heuristic 3: Avoid mapping relationships across Aggregate Roots; simply map the foreign key as is.
By adhering to this heuristic, we would only include Long customerId within the Order entity. To retrieve both customer information and their associated orders, two separate repository calls would be necessary.
Sometimes, it is more convenient to have a relationship rather than making repeated repository calls. However, mapping to another Aggregate Root should only be considered when the number of related entities is finite. Specifically, this means either a *ToOne relationship or a *ToMany relationship where the business logic restricts growth beyond a certain limit.
Heuristic 4: Relationships to another Aggregate Root should only be mapped if they are bounded.
For example, if ProductCategory is also an Aggregate Root (requiring management and access control), then mapping the association from Product to ProductCategory is acceptable as it is bounded—each product can only belong to a single category in this scenario. Conversely, the relationship between Customer and Order can expand infinitely as customers place more orders.
Unidirectional vs. Bidirectional Relationships
While it may seem advantageous to create bidirectional relationships for added flexibility, this approach can lead to complications in maintaining synchronization.
Heuristic 5: Start with Unidirectional Relationships. Transition to Bidirectional Relationships only when necessary and when they do not cross Aggregate Roots.
For instance, a bidirectional relationship might be appropriate between Order and OrderItem, as Order is the aggregate root.
Lazy vs. Eager Fetching
Lazy loading is a design pattern that defers the initialization of an object until absolutely necessary. For a comprehensive discussion, refer to prior articles.
When multiple business use cases require querying one of the Aggregate Roots, it is essential to manage which data from associated entities is retrieved based on the context of each use case. Controlling associations within the relationship hierarchy can be achieved through various strategies (as discussed previously).
Heuristic 6: Begin with Lazy Fetching for all child relationships from the Aggregate Root. Switch to Eager Fetching only if the related entity is ALWAYS REQUIRED.
Since all queries commence with an aggregate root, another aggregate root would only be accessed through original children in ToOne or bounded ToMany relationships. These scenarios may necessitate lazy loading to prevent deeply nested loads caused by eager fetching of other aggregate root objects.
Heuristic 7: If you have a relationship to another Aggregate Root, refrain from using eager fetching.
Cascading
JPA cascading allows you to define how operations performed on one entity should propagate to related entities. When using cascading in JPA, you specify that an operation (like persist, merge, or remove) executed on a parent entity should also apply to its child entities automatically. This is indicated by the cascade parameter in the relationship annotation.
If we have accurately defined our entities and aggregate root entities, the heuristic for cascading becomes straightforward:
Heuristic 8: Cascade ALL non-aggregate root entities. Do not cascade for associated aggregate entities.
Addressing What Ifs
Here are some scenarios and my recommendations for each. These are guidelines rather than strict rules.
A business requirement spans two aggregate roots.
Example: Generate a list of orders for customers residing in California. Solution: First, query for customer IDs in California, then retrieve orders that match those customer IDs.
A new requirement elevates an entity from a non-root aggregate to an aggregate root.
Example: Product images uploaded by administrators necessitate direct querying capabilities. Solution 1: Update the mapping to align with the guidelines for aggregate roots. Solution 2: This could define a new bounded context, suggesting the separation of the feature into another microservice (e.g., Image Admin Microservice vs. Shopping Microservice).
Conclusion
In this discussion, we examined how to integrate Domain-Driven Design principles into JPA and Hibernate relationship design.
Here are the eight heuristics summarized:
- Heuristic 1: Spring Repositories should be defined only for Aggregate Root Entities.
- Heuristic 2: Within an Aggregate, consider using Value Objects instead of @OneToOne or @ManyToOne.
- Heuristic 3: Avoid mapping relationships across Aggregate Roots; map the foreign key instead.
- Heuristic 4: Only map relationships to another Aggregate Root if they are bounded.
- Heuristic 5: Start with Unidirectional Relationships; convert to Bidirectional Relationships only as necessary and when they do not cross Aggregate Roots.
- Heuristic 6: Begin with Lazy Fetching for all child relationships from the Aggregate Root; switch to Eager Fetching only if the related entity is ALWAYS REQUIRED.
- Heuristic 7: If you have a relationship to another Aggregate Root, do not use eager fetching.
- Heuristic 8: Cascade ALL non-aggregate root entities; do not cascade associated aggregate entities.
I welcome any additional heuristics you might have. In our next discussion, we will explore other pitfalls to avoid in JPA and Hibernate.