1. Overview
In this article, we’ll revisit key Domain-Driven Design (DDD) concepts and demonstrate how to use jMolecules to express these technical concerns as metadata.
We will explore how this approach benefits us and discuss jMolecules’ integration with popular libraries and frameworks from the Java and Spring ecosystem.
Finally, we’ll focus on ArchUnit integration and learn how to use it to enforce a code structure that adheres to DDD principles during the build process.
2. The Goal of jMolecules
jMolecules is a library that allows us to express architectural concepts explicitly, enhancing code clarity and maintainability. The authors’ research paper provides a detailed explanation of the project’s goals and main features.
In summary, jMolecules helps us keep the domain-specific code free from technical dependencies and expresses these technical concepts through annotations and type-based interfaces.
Depending on the approach and design we choose, we can import the relevant jMolecules module to express technical concepts specific to that style. For instance, here are some supported design styles and the associated annotations we can use:
- Domain-Driven Design (DDD): Use annotations like @Entity, @ValueObject, @Repository, and @AggregateRoot
- CQRS Architecture: Utilize annotations such as @Command, @CommandHandler, and @QueryModel
- Layered Architecture: Apply annotations like @DomainLayer, @ApplicationLayer, and @InfrastructureLayer
Furthermore, this metadata can then be used by tools and plugins for tasks like generating boilerplate code, creating documentation, or ensuring the codebase has the correct structure. Even though the project is still in its early stages, it supports integrations with various frameworks and libraries.
For instance, we can import the Jackson and Byte-Buddy integrations to generate boilerplate code or include JPA and Spring-specific modules to translate jMolecules annotations into their Spring equivalents.
3. jMolecules and DDD
In this article, we’ll focus on the DDD module of jMolecules and use it to create the domain model of a blogging application. Firstly, let’s add the jmolecumes-starter-ddd and jmolecules-starter-test dependencies to our pom.xml:
<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-starter-ddd</artifactId>
<version>0.21.0</version>
</dependency>
<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-starter-test</artifactId>
<version>0.21.0</version>
<scope>test</scope>
</dependency>
In the code examples below, we’ll notice similarities between jMolecules annotations and those from other frameworks. This happens because frameworks like Spring Boot or JPA follow DDD principles as well. Let’s briefly review some key DDD concepts and their associated annotations.
3.1. Value Objects
A value object is an immutable domain object that encapsulates attributes and logic without a distinct identity. Moreover, value objects are defined solely by their attributes.
In the context of articles and blogging, an article’s slug is immutable and can handle its own validation upon creation. This makes it an ideal candidate for being marked as a @ValueObject:
@ValueObject
class Slug {
private final String value;
public Slug(String value) {
Assert.isTrue(value != null, "Article's slug cannot be null!");
Assert.isTrue(value.length() >= 5, "Article's slug should be at least 5 characters long!");
this.value = value;
}
// getter
}
Java records are inherently immutable, making them an excellent choice for implementing value objects. Let’s use a record to create another @ValueObject to represent an account Username:
@ValueObject
record Username(String value) {
public Username {
Assert.isTrue(value != null && !value.isBlank(), "Username value cannot be null or blank.");
}
}
3.2. Entities
Entities differ from value objects in that they possess a unique identity and encapsulate mutable state. They represent domain concepts that require distinct identification and can be modified over time while maintaining their identity throughout different states.
For example, we can imagine an article comment as an entity: each comment will have a unique identifier, an author, a message, and a timestamp. Furthermore, the entity can encapsulate the logic needed to edit the comment message:
@Entity
class Comment {
@Identity
private final String id;
private final Username author;
private String message;
private Instant lastModified;
// constructor, getters
public void edit(String editedMessage) {
this.message = editedMessage;
this.lastModified = Instant.now();
}
}
3.3. Aggregate Roots
In DDD, aggregates are groups of related objects that are treated as a single unit for data changes and have one object designated as the root within the cluster. The aggregate root encapsulates the logic to ensure that changes to itself and all related entities occur within a single atomic transaction.
For instance, an Article will be an aggregate root for our model. An Article can be identified using its unique slug, and will be responsible for managing the state of its content, likes, and comments:
@AggregateRoot
class Article {
@Identity
private final Slug slug;
private final Username author;
private String title;
private String content;
private Status status;
private List<Comment> comments;
private List<Username> likedBy;
// constructor, getters
void comment(Username user, String message) {
comments.add(new Comment(user, message));
}
void publish() {
if (status == Status.DRAFT || status == Status.HIDDEN) {
// ...other logic
status = Status.PUBLISHED;
}
throw new IllegalStateException("we cannot publish an article with status=" + status);
}
void hide() { /* ... */ }
void archive() { /* ... */ }
void like(Username user) { /* ... */ }
void dislike(Username user) { /* ... */ }
}
As we can see, The Article entity is the root of an aggregate that includes the Comment entity and some value objects. Aggregates cannot directly reference entities from other aggregates. So, we can only interact with the Comment entity through the Article root, and not directly from other aggregates or entities.
Additionally, aggregate roots can reference other aggregates through their identifiers. For example, the Article references a different aggregate: the Author. It does this through the Username value object, which is a natural key of the Author aggregate root.
3.4. Repositories
Repositories are abstractions that provide methods for accessing, storing, and retrieving aggregate roots. From the outside, they appear as simple collections of aggregates.
Since we defined Article as our aggregate root, we can create the Articles class and annotate it with @Repository. This class will encapsulate the interaction with the persistence layer and provide a Collection-like interface:
@Repository
class Articles {
Slug save(Article draft) {
// save to DB
}
Optional<Article> find(Slug slug) {
// query DB
}
List<Article> filterByStatus(Status status) {
// query DB
}
void remove(Slug article) {
// update DB and mark article as removed
}
}
4. Enforcing DDD Principles
Using jMolecules annotations lets us define architectural concepts in our code as metadata. As previously discussed, this enables us to integrate with other libraries to generate boilerplate code and documentation. However, in the scope of this article, we’ll focus on enforcing the DDD principles using the archunit and jmolecules-archunit:
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-archunit</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
Let’s create a new aggregate root and intentionally break some core DDD rules. For instance, we can create an Author class – without an identifier – that references an Article directly by object reference instead of using the article’s Slug. Additionally, we can have an Email value object that includes the Author entity as one of its fields, which would also violate DDD principles:
@AggregateRoot
public class Author { // <-- entities and aggregate roots should have an identifier
private Article latestArticle; // <-- aggregates should not directly reference other aggregates
@ValueObject
record Email(
String address,
Author author // <-- value objects should not reference entities
) {
}
// constructor, getter, setter
}
Now, let’s write a simple ArchUnit test to validate the structure of our code. The main DDD rules are already defined via JMoleculesDddRules. So, we just need to specify the packages we want to validate for this test:
@AnalyzeClasses(packages = "com.baeldung.dddjmolecules")
class JMoleculesDddUnitTest {
@ArchTest
void whenCheckingAllClasses_thenCodeFollowsAllDddPrinciples(JavaClasses classes) {
JMoleculesDddRules.all().check(classes);
}
}
If we try to build the project and run the test, we’ll see the following violations:
Author.java: Invalid aggregate root reference! Use identifier reference or Association instead!
Author.java: Author needs identity declaration on either field or method!
Author.java: Value object or identifier must not refer to identifiables!
Let’s fix the mistakes and make sure our code complies with the best practices:
@AggregateRoot
public class Author {
@Identity
private Username username;
private Email email;
private Slug latestArticle;
@ValueObject
record Email(String address) {
}
// constructor, getters, setters
}
5. Conclusion
In this tutorial, we discussed separating technical concerns from business logic and the advantages of explicitly declaring these technical concepts. We found that jMolecules helps achieve this separation and enforces best practices from an architectural perspective, based on the chosen architectural style.
Additionally, we revisited key DDD concepts and used aggregate roots, entities, value objects, and repositories to draft the domain model of a blogging website. Understanding these concepts helped us create a robust domain, and jMolecules’ integration with ArchUnit enabled us to verify best DDD practices.
As always, the code for these examples is available over on GitHub.