Domain-Driven-Design With JPA: A Practical Guide
A practical guide to DDD with JPA in a modular monolithic architecture by leveraging ORM annotations and addressing some pitfalls.
Join the DZone community and get the full member experience.
Join For Free
Domain-Driven Design (DDD) is a powerful approach to software development that emphasizes the importance of the domain and its logic. However, propagating it to the database entity design is not a usual habit and can be quite tricky. This article explores the principles and implementation strategies of DDD with Java Persistence API (JPA), offering practical insights for developers.
Some Codebase Context
In the following chapters, we consider the project as monolithic, but split into several modules (Maven ones), each one dealing with a sub-domain of the backend. No dependency exists between them, at least no entity-related ones, to respect the Single Responsibility Principle (SRP) that goes along with DDD. Thus, the idea is to make the database entities of each sub-domain cohabitate with other sub-domain entities while sharing a single database schema. As a consequence, the entities will kind of overlap, but without being tangled altogether. Hence, to be aligned with DDD and maintain proper domain boundaries and responsibilities, the key idea is to look at them as being some facets of the database tables.
The Core Principles
The very main principle is to apply Single Responsibility Principle (SRP) for writing data: only one sub-domain must be in charge of writing into a column of the database. Other sub-domains will either not consider the column or only read it.
The second principle is to allow read overlap: several domains may access the same column in read-only mode.
As you see, we don't need to apply replication here. Even if this is still possible, the objective is to manage a normalized-form database.
Best Practices Before Starting
A highly advisable practice is to design the entity graph as a tree: the entities must only be accessed by the root of the aggregate; accessing the entities from any "side" of it is forbidden. Here are some benefits to it:
-
the major one is that it usually helps your ORM to handle the persistence because you can afford to set everything as eagerly loaded (avoiding N+1 problems), and it also helps to have some better SQL queries that use joins.
-
it clarifies the code and lets you align your domain name with the root of the aggregate.
A major other piece of advice is also to master your ORM:
-
learn the different forms of inheritance mapping (usual ones, polymorphic ones) and their impact on the database design
-
master its annotations, for example, when joining tables (you may tweak the default joined columns) or composing your entity with
@Embeddableor@SecondaryTable -
choose the appropriate identifier policy (be aware that some JPA policies don't have the same result on the database depending on the support of auto-increment, sequence, and so on), especially for read-only entities; the already-assigned one is suitable.
Some Tips
As mentioned above, one very useful tip is to set some properties as read-only. To do this, JPA allows marking columns as "insertable false" and "updatable false". Hence, to avoid a column being written, just mark it with both as in the example below:
Java
@Entity
public class Equipment {
@Column(updatable=false, insertable=false)
private String name;
@Column(updatable=false, insertable=false)
private String vendor;
}
When dealing with relations, we apply the same approach by skipping cascading:
Java
@Entity
public class Stock {
@OneToMany // no cascade => no-operation is applyied to the collection
private Set<Equipment> equipments;
}
A usually known one, but worth mentioning for our goal, is that you can decorrelate entity and column names from their database table mirror. This is quite important because Domain Driven Design tries to fit with the business domain vocabulary. Hence, you may have some slight differences between your sub-domains, like the two sides of the same coin.
The last tip would require many examples due to the many combinations it allows, hence we will only enumerate and remind the existence of some annotations that may be very helpful while defining entities structure:
The last tip is actually a bunch of them, but all deal with defining entities' structure:
-
@Embeddableis an easy one that, for our case, can push aside some columns with some specific concepts or make the main entity lighter, whereas the original concept is much more complete in another sub-domain and, overall, is fully a part of an entity. -
@SecondaryTablecan replace@Inheritance(JOINED)in some read circumstances depending on the design or complexity you have to deal with. -
@Inheritance(SINGLE_TABLE)permits separating different concepts from the same table into a clear hierarchy; however, you must be careful with such annotation because it requires filling the mandatory columns that might not be part of your entities.
Unfortunately, we can't explore here all the structuring annotations and their possibilities because there are so many of them as well as potential combinations. However, I hope to have given you some ideas and provided some new regard on JPA's annotations.
Pitfalls
As mentioned at the beginning, if the project is monolithic and made of sub-modules, you may have an aggregator on top of it, which is usually Spring Boot if we consider Java as the main target of this article. Then you should be aware of some drawbacks of such an approach:
-
Because JPA forbids having the same entity name twice in the same persistence context, you will need to name them with
@Entity(name="...")to overcome the name conflict. You can simply prefix the entity name with your sub-domain name to avoid "duplicates." -
If you prefer having several
@PersistenceContext(one per sub-domain), you'll have to name them and inject the right one in your services. Also, you must make it scan the right sub-packages; otherwise, it will iterate over the whole classpath and generate a humongous persistence context containing all the entities of your sub-domains. -
Finally, here comes the tests and read-only properties. If you segregate your domains and no one sees each other, you will struggle to insert data easily, simply because your JPA context is not allowed to fill the read-only columns. Then you have 2 choices here: either do it with some low-level access (JDBC, script, etc.) or... create yet-another-entity-model dedicated to database filling for the tests.
Conclusion
Adhering to the principles of Domain-Driven Design (DDD) and the Single Responsibility Principle (SRP) when developing your JPA entities represents a valuable and robust strategy for creating maintainable code. In this article we have learned several techniques that allow the application of these principles within the codebase; however, they necessitate a profound understanding of JPA. Furthermore, one must be mindful and vigilant regarding potential pitfalls that may arise when integrating various sub-modules that use this methodology.
Meanwhile, my experience showed me that this investment is worthwhile in the long run, as it has enabled us to decouple different sub-domain entities without resorting to microservices. This technique can be a powerful challenger to CRUD backends.
Database
Design
Domain-driven design
Opinions expressed by DZone contributors are their own.
Comments