The article is a long read. I recommend you to look through the Table of contents
in advance. Perhaps some parts can be more intriguing than others.
I like Hibernate. Even though it's complicated (and error-prone sometimes), I consider it an extremely useful framework. If you know how to cook it, it shines.
Though Hibernate is in implementation of JPA, I'm going to say Hibernate every time I mean JPA. Just for the sake of simplicity.
Lots of Java developers use Hibernate in their projects. However, I’ve noticed a strange tendency. Java devs apply Anemic Domain Model pattern almost every time. Whenever I ask about reasons for picking up that strategy, I get answers like:
- We've always done it before and it works fine.
- Our developers got used to such architecture.
- Do we have any other options?
That's why I've decided to come up with this article. I want to reconsider the status quo of Anemic Domain Model usage with Hibernate and propose you something different. And that is Rich Domain Model pattern.
In this article, I'm telling you:
- What is Rich Domain Model?
- What's wrong with Anemic Domain Model and how can Rich one fix the issues?
- Step by step solution.
- What disadvantages does Rich Domain Model have?
You can check out the entire repository with code examples by this link.
Table of contents
- The problems with Anemic Domain Model
- Too smart services
- Possible invariants' violation
- Lack of encapsulation
- Testing is harder
- Rich Domain Model principle
- Don't add setters, getters, and public no-args constructor
- No-args constructor allows incostistent object instantiation
- Setters break encapsulation
- Getters break encapsulation
- Current result
- Aggregate and Aggregate root
- Evolution of requirements
- Each Pocket must possess at least one Tamagotchi
- Every Tamagotchi name has to be unique within a Pocket
- If a user deletes a Tamagotchi, they can restore it by name
- Querying data
- Manual queries
- Introducing toDto method
- Unit testing entities
- Pocket must always possess at least one Tamagotchi
- If you delete Tamagotchi, you can restore it by name
- If you delete multiple Tamagotchi with the same name, you can only restore the last one
- Integration testing
- Create Pocket
- Create Tamagotchi
- Update Tamagotchi
- Performance implications
- Optimization of queries
- Pinpointing optimized checks
- Database generated ID
- Manually fill the id
- Introducing business key
- Is Rich Domain Model always worth it?
- Conclusion
- Resources
The problems with Anemic Domain Model
Firstly, let’s discuss the domain. We’re going to develop the Tamagotchi application. Pocket may have many Tamagotchi
instances, but each Tamagotchi
belongs to a single Pocket
. Therefore, the relationship is Pocket --one-to-many-> Tamagotchi
.
Most likely the Anemic Domain Model solution would be implemented in this way:
I bet you’ve seen a lot of similar Java code. But the solution contains many problems. Let’s discuss them one by one.
Too smart services
Anemic Domain Model requires the service layer to possess all business logic. While entities act as dummy data structures. But entities are not static. They develop during the time. We may add some fields and delete others. Or we can combine existing fields in Embeddable
object.
Here, services have to know every minor detail of the entity they are working with. Because any operation may require access to different fields. Meaning that even a slight change in an entity may lead to major restructuring in many services. Actually, that breaks Open-Closed principle. The code becomes not object-oriented but procedural. We don’t use benefits of OOP paradigm. Instead, we bring additional difficulties.
Possible invariants' violation
Invariant is a business rule that allows only certain changes to entities. It guarantees that we won’t transmit entities to the wrong state. For example, suppose that Pocket
may contain only three Tamagotchi
by default. If you want to have more, you need to buy premium subscription. That’s an invariant. The code has to disallow adding fourth Tamagotchi
to Pocket
, if user don’t purchase the additional feature.
If we choose the Anemic Domain Model approach, it means that services are obligatory to check invariants and cancel operation if needed. But invariants are also not static. Imagine that the rule of three Tamagotchi
within Pocket
has not been introduced from the start of the project. But we want to add it now. It means that we have to check every method and function that might create a new Tamagotchi
and add corresponding checks.
It becomes even worse if changes are broader. Suppose that Tamagotchi
has become a part of Saga pattern. Now it contains status
field that has a value of PENDING
. If Tamagotchi
is PENDING
, you can neither delete it nor update it. Do you see where I’m going? You have to check every piece of code that updates or deletes Tamagotchi
and make sure you don’t miss any check for PENDING
status.
Lack of encapsulation
Encapsulation in OOP is a mechanism that restricts direct access to certain data. That makes sense. Entity might have several fields, but it doesn’t mean we want to allow changing each of them. We might change only simultaneously concrete fields. Other ones are allowed to be updated only if the entity transmits to a specific state.
Anemic Domain Model forces us to give up encapsulation and put @Getter
and @Setter
annotations from Lombok without considering the consequences.
And the biggest problem with violating encapsulation is that code becomes more dangerous to work with. You cannot just call setName
or setStatus
methods. But you have to make sure that you check specific conditions in advance. Again, invariants aren’t static. So, every mutation call to an entity is like a land mine. You don’t know what breaks next, if you miss a single condition check.
Testing is harder
Mostly developers use Hibernate in combination with Spring Boot. Meaning that services are regular Spring beans with @Transactional annotation. Usually those services contain spaghetti code of entities, repositories, and other services invocations. When it comes to testing, I see developers choose one of options:
- Integration testing.
- Mocking everything.
Don’t get me wrong. I think that integration testing is crucial. And Tescontainers library especially helped to make the process smooth. However, I think that the count of integration tests should be as minimum as possible. If you can validate something with a simple unit test, do it this way. Bringing too much integration tests into the project also leads to certain difficulties:
- Integration tests are harder to maintain.
- There is always a shared resource (database, in this case). So, tests might unexpectedly become dependent on each other. Tests might get flaky.
- It's tough to run integration tests in parallel.
- Those tests are much slower. If your project is old enough and you have many integration tests, a regular CI build can run in 30 minutes or even more.
What about mocking? I think such tests are almost useless. I don’t mean that mocking in general is a bad idea. But if you try to mock every call to Spring Data JPA repository and other service, it may occur that you don’t test the behaviour. You just verify the correct order of mocks’ invocations. So, tests become fragile and an enormous burden to maintain.
Rich Domain Model principle
On the contrary, Rich Domain Model pattern proposes a different approach. Look at the diagram below.
As you can see, entities hold the required business logic. While services act like a thin layer that delegates call to repositories and entities.
Rich Domain Model correlates with tactical patterns of Domain Driven Design. The one that we're interested in is aggregate.
Aggregate is a cluster of domain objects that you can treat as a whole unit. For example, Pocket
has many Tamagotchis
. Meaning that Pocket
and Tamagotchi
can be a single aggregate. Aggregate root is the entity that allows direct access to aggregate and guarantees invariants’ correctness. Therefore, if we want to change something in Tamagotchi
, we should only interact with Pocket
.
By introducing Rich Domain Model, I want to solve these problems:
- Code should become more business-oriented. If logic is divided between many services, it's hard to understand the actual operation flow (especially for newcomers).
- Let the compiler validate your code. If an entity has a setter for every field, you should put additional check whenever you invoke it. But if an entity provides only certain amount of methods that mutates its state, it means that incorrect transition is not possible due to compiler check. In another words, if a method doesn't exist, you cannot call it. So, it's better to provide only those operations that are needed.
- Reduce the amount of integration tests. If it's possible, it's better to test business logic with simple unit tests. So, I want to replace some integration tests with unit ones without violating the quality assurance level.
Let's start our journey with refactoring Pocket
and Tamagotchi
to Rich Domain Model.
Don't add setters, getters, and public no-args constructor
Firstly, look at the initial approach of designing Pocket
and Tamagotchi
entities following Anemic Domain Model:
@Entity
@NoArgsConstructor
@Setter
@Getter
public class Pocket {
@Id
private UUID id;
private String name;
@OneToMany(mappedBy = "pocket")
private List<Tamagotchi> tamagotchis = new ArrayList<>();
}
@Entity
@NoArgsConstructor
@Setter
@Getter
public class Tamagotchi {
@Id
private UUID id;
private String name;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "pocket_id")
private Pocket pocket;
}
Here I’m using UUID as a primary key. I understand that there are some performance implications for it. But now client-side generated ID is crucial for a smooth transition to the Rich Domain Model. Anyway, later I’ll give you some examples with other ID types.
I bet this looks familiar. Perhaps your current project contains lots of similar declarations. What problems are there?
No-args constructor allows incostistent object instantiation
Hibernate demands each entity to provide a no-args constructor. Otherwise, the framework doesn’t work properly. And it’s one of the edgy cases that can make your code less straight-forward and more buggy.
Thankfully, there is a solution. Hibernate doesn’t need a public constructor for an entity. Instead, it can be protected. So, we can add a public static method to instantiate the entity, and leave protected constructor for Hibernate specifically. Look at the code example below:
@Entity
@NoArgsConstructor(access = PROTECTED)
@Setter
@Getter
public class Pocket {
@Id
private UUID id;
private String name;
@OneToMany(mappedBy = "pocket")
private List<Tamagotchi> tamagotchis = new ArrayList<>();
public static Pocket newPocket(String name) {
Pocket pocket = new Pocket();
pocket.setId(UUID.randomUUID());
pocket.setName(name);
return pocket;
}
}
@Entity
@NoArgsConstructor(access = PROTECTED)
@Setter
@Getter
public class Tamagotchi {
@Id
private UUID id;
private String name;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "pocket_id")
private Pocket pocket;
public static Tamagotchi newTamagotchi(String name, Pocket pocket) {
Tamagotchi tamagotchi = new Tamagotchi();
tamagotchi.setId(UUID.randomUUID());
tamagotchi.setName(name);
tamagotchi.setPocket(pocket);
return tamagotchi;
}
}
As you can see, business code (that is likely to be in a different package) cannot instantiate Tamagotchi
or Pocket
with no-args constructor. It has to invoke dedicated methods newTamagotchi
and newPocket
that accept a specific amount of parameters.
Setters break encapsulation
I think public setters aren’t much different from regular public fields. Well, you could put some checks in a setter because it’s a method. But in reality, people tend not to go this way. Usually we just put @Setter
annotation from Lombok library on top of class and that’s it.
I consider using setters in an entity a bad approach due to these reasons:
- Possible invariants’ violation. Some fields cannot be updated. Other ones can be updated only if the entity is being transmitted to a particular state. Pure setters forces developer to put all those checks in services.
- If
Tamagotchi.name
isString
, it doesn't mean that everyString
value is allowed. Therefore, you also have to perform those checks outsided of an entity. - A field can be a part of implementation detail. Maybe it's forbidden to update it directly. But a public setter allows this operation.
The main point is that public setters breaks the principle of compiler validation that I mentioned previously. You just provide too many options that can be called differently.
What’s the alternative? I suggest adding changeXXX
methods for specific behaviour. Also, those methods should contain validation logic and throw exception if needed.
Suppose that Tamagotchi
entity has a status
field that can have the value of PENDING
. If Tamagotchi
is PENDING
, it cannot be modified. Look at the code example below:
@Entity
@NoArgsConstructor(access = PROTECTED)
@Getter
public class Tamagotchi {
@Id
private UUID id;
private String name;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "pocket_id")
private Pocket pocket;
@Enumerated(STRING)
private Status status;
public void changeName(String name) {
if (status == PENDING) {
throw new TamagotchiStatusException("Tamagotchi cannot be modified because it's PENDING");
}
if !(nameIsValid(name)) {
throw new TamagotchiNameInvalidException("Invalid Tamagotchi name: " + name);
}
this.name = name;
}
public static Tamagotchi newTamagotchi(String name, Pocket pocket) { /* entity creation */ }
}
The Tamagotchi.changeName
method guarantees that you cannot change name
if certain preconditions are violated. The code that invokes the method doesn’t need to know about specific rules. You just have to deal with exceptions.
Getters break encapsulation
Well, the previous paragraph about setters is more or less obvious. There are dozens of articles and opinions on the Internet about problems with setters. Anyway, eliminating getters sounds ridiculous, isn’t it? They don’t mutate the state of an entity. So, what’s the deal?
The problem with getters is that they also allow to break encapsulation and perform unnecessary or wrong checks. Suppose that we also want to restrict updating the name of Tamagotchi
if its status is ERROR
. That's the possible solution you might see during the code review:
@Service
@RequiredArgsConstructor
public class TamagotchiService {
private final TamagotchiRepository repo;
@Transactional
public void changeName(UUID id, String name) {
Tamagotchi tamagotchi = repo.findById(id).orElseThrow();
if (tamagotchi.getStatus() == ERROR) {
throw new TamagotchiStatusException("Tamagotchi cannot be modified because its status is ERROR");
}
tamagotchi.changeName(name);
}
}
Though Tamagotchi
provides a dedicated method changeName
, the check is still implemented in the service layer. I've noticed that even experienced senior developers tend to fall into anemic model mindset when there is a possibility. Because they've been working for years on different projects and most likely each one has applied Anemic Domain Model pattern. So, developers just choose the simpler and more obvious way.
However, a decision has some consequences. Firstly, the logic is divided between Tamagotchi
entity and TamagotchiService
(that’s the one thing we’ve wanted to avoid). Secondly, checks might be duplicated and you can miss it during the code review. And finally, some checks can be outdated in time. For example, this validation of ERROR
status might become obsolete later. If you forget to eliminate it here, your code won’t act expectedly.
As I mentioned before, if you don’t need a method, just don’t add it. Getters aren’t required to perform business logic. You can put validations inside Tamagotchi.changeName
method. If a getter is not present, it cannot be invoked and such a scenario won’t happen.
What about querying, then? Usually we use Hibernate entities to
SELECT
data, transform it into DTO, and return the result to the user. How can we do it without getters? Don’t worry, we’ll discuss this topic later in the article.
There is also one exception for this rule. You can add getters for ID. Sometimes it’s necessary to know the entity id in runtime. Later you’ll see an example of that.
Current result
We've already discussed three points:
- No-args constructor.
- Setters.
- Getters.
If we remove those pieces, the code will look like this:
@Entity
@NoArgsConstructor(access = PROTECTED)
public class Pocket {
@Id
private UUID id;
private String name;
@OneToMany(mappedBy = "pocket")
private List<Tamagotchi> tamagotchis = new ArrayList<>();
public static Pocket newPocket(String name) {
Pocket pocket = new Pocket();
pocket.setId(UUID.randomUUID());
pocket.setName(name);
return pocket;
}
}
@Entity
@NoArgsConstructor(access = PROTECTED)
public class Tamagotchi {
@Id
private UUID id;
private String name;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "pocket_id")
private Pocket pocket;
@Enumerated(STRING)
private Status status;
public void changeName(String name) {
if (status == PENDING) {
throw new TamagotchiStatusException("Tamagotchi cannot be modified because it's PENDING");
}
if (!nameIsValid(name)) {
throw new TamagotchiNameInvalidException("Invalid Tamagotchi name: " + name);
}
this.name = name;
}
public static Tamagotchi newTamagotchi(String name, Pocket pocket) {
Tamagotchi tamagotchi = new Tamagotchi();
tamagotchi.setId(UUID.randomUUID());
tamagotchi.setName(name);
tamagotchi.setPocket(pocket);
tamagotchi.setStatus(CREATED);
return tamagotchi;
}
}
Aggregate and Aggregate root
Previously I’ve mentioned the Aggregate pattern. Speaking about our domain, the Pocket
entity should be the Aggregate root. However, existing API allows us to access Tamagotchi
entity directly. Let’s fix that.
Firstly, let's add simple CREATE/UPDATE/DELETE
operations. Look at the code example below:
@Entity
@NoArgsConstructor(access = PROTECTED)
public class Pocket {
@Id
private UUID id;
private String name;
@OneToMany(mappedBy = "pocket", cascade = PERSIST, orphanRemoval = true)
private List<Tamagotchi> tamagotchis = new ArrayList<>();
public UUID createTamagotchi(TamagotchiCreateRequest request) {
Tamagotchi tamagotchi = Tamagotchi.newTamagotchi(request.name(), this);
tamagotchis.add(tamagotchi);
return tamagotchi.getId();
}
public void updateTamagotchi(UUID tamagotchiId, TamagotchiUpdateRequest request) {
Tamagotchi tamagotchi = tamagotchiById(tamagotchiId);
tamagotchi.changeName(request.name());
}
public void deleteTamagotchi(UUID tamagotchiId) {
Tamagotchi tamagotchi = tamagotchiById(tamagotchiId);
tamagotchis.remove(tamagotchi);
}
private Tamagotchi tamagotchiById(UUID tamagotchiId) {
return tamagotchis
.stream()
.filter(t -> t.getId().equals(tamagotchiId))
.findFirst()
.orElseThrow(() -> new TamagotchiNotFoundException("Cannot find Tamagotchi by ID=" + tamagotchiId));
}
public static Pocket newPocket(String name) {
Pocket pocket = new Pocket();
pocket.setId(UUID.randomUUID());
pocket.setName(name);
return pocket;
}
}
@Entity
@NoArgsConstructor(access = PROTECTED)
class Tamagotchi {
@Id
@Getter
private UUID id;
private String name;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "pocket_id")
private Pocket pocket;
@Enumerated(STRING)
private Status status;
public void changeName(String name) {
if (status == PENDING) {
throw new TamagotchiStatusException("Tamagotchi cannot be modified because it's PENDING");
}
if (!nameIsValid(name)) {
throw new TamagotchiNameInvalidException("Invalid Tamagotchi name: " + name);
}
this.name = name;
}
public static Tamagotchi newTamagotchi(String name, Pocket pocket) {
Tamagotchi tamagotchi = new Tamagotchi();
tamagotchi.setId(UUID.randomUUID());
tamagotchi.setName(name);
tamagotchi.setPocket(pocket);
tamagotchi.setStatus(CREATED);
return tamagotchi;
}
}
There are a lot of nuances. So, I’ll point out each of them one by one. Firstly, Pocket
entity provides methods createTamagotchi
, updateTamagotchi
, and deleteTamagotchi
as-is. You don’t retrieve any information from Tamagotchi
or Pocket
. You just invoke the required functionality.
I’m aware that such a technique also has performance penalties. We’ll also discuss some approaches to overcome these problems later.
Then goes Tamagotchi
entity. The first thing I want you to notice is that the entity is package-private. Meaning that nobody can access Tamagotchi
outside of the package. Therefore, calling Pocket
directly is the only way.
Now you may think that its profit isn’t so obvious. But soon we’ll discuss the evolution of aggregate and you’ll see the benefits.
Neither Pocket
nor Tamagotchi
entity provides regular setters or getters. One can only invoke public methods of Pocket
entity.
Evolution of requirements
As I said before, entities aren't static. Requirements change and invariants as well. So, let's look through a hypothetical process of implementing new requirements and see how it goes.
Each Pocket must possess at least one Tamagotchi
It means that we should create a Tamagotchi
, when a new Pocket
is instantiated. Also, if you want to delete a Tamagotchi
, you have to check that it’s not the single one within the Pocket
. Look at the code example below:
@Entity
@NoArgsConstructor(access = PROTECTED)
public class Pocket {
/* fields and other methods */
public void deleteTamagotchi(UUID tamagotchiId) {
Tamagotchi tamagotchi = tamagotchiById(tamagotchiId);
if (tamagothis.size() == 1) {
throw new TamagotchiDeleteException("Cannot delete Tamagotchi because it's the single one");
}
tamagotchis.remove(tamagotchi);
}
public static Pocket newPocket(String name) {
Pocket pocket = new Pocket();
pocket.setId(UUID.randomUUID());
pocket.setName(name);
pocket.createTamagotchi(new TamagotchiCreateRequest("Default")); // creating default tamagotchi
return pocket;
}
}
As you can see, invariants’ correctness is guaranteed within an aggregate. Even if you want to, you cannot create a Pocket
with zero Tamagotchi
or delete Tamagotchi
if it’s a single one. And I think that it’s great. Code becomes less error-prone and easier to maintain.
Every Tamagotchi name has to be unique within a Pocket
To implement this requirement, we need to alter createTamagotchi
and updateTamagotchi
methods a bit. Look at the code example below:
@Entity
@NoArgsConstructor(access = PROTECTED)
public class Pocket {
/* fields and other methods */
public UUID createTamagotchi(TamagotchiCreateRequest request) {
Tamagotchi tamagotchi = Tamagotchi.newTamagotchi(request.name(), this);
tamagotchis.add(tamagotchi);
validateTamagotchiNamesUniqueness();
return tamagotchi.getId();
}
public void updateTamagotchi(UUID tamagotchiId, TamagotchiUpdateRequest request) {
Tamagotchi tamagotchi = tamagotchiById(tamagotchiId);
tamagotchi.changeName(request.name());
validateTamagotchiNamesUniqueness();
}
private void validateTamagotchiNamesUniqueness() {
Set<String> names = new HashSet<>();
for (Tamagotchi tamagotchi : tamagotchis) {
if (!names.add(tamagotchi.getName()) {
throw new TamagotchiNameInvalidException("Tamagotchi name is not unique: " + tamagotchi.getName());
}
}
}
}
You’ve probably noticed that I added a getter for
Tamagotchi.name
field. BecauseTamagotchi
form a single aggregate, it’s fine to provide getters. BecauseTamagotchi
should not request anything fromI understand that
validateTamagotchiNamesUniqueness
doesn't perform well. Don't worry, we'll discuss workarounds later in thePerformance implications
section.
Once again, the domain model guarantees that each Tamagotchi
name is unique within a Pocket
. What is interesting is that API hasn’t changed a bit. The code that invokes those public methods (likely domain services) doesn’t have to change logic.
If a user deletes a Tamagotchi, they can restore it by name
This one is tricky and involves soft deletion. It also has additional points:
- If
Tamagotchi
with the same name already exists, a user cannot restore the one they've deleted. - If a user deletes multiple
Tamagotchis
with the same name, they can only restore the last one.
I'm not a fan of soft deletion that involves adding isDeleted
column by many reasons. Instead, I will introduce a new entity DeletedTamagotchi
that contains the state of deleted Tamagotchi
. Look at the code example below.
@Entity
@NoArgsConstructor(access = PROTECTED)
@Getter
class DeletedTamagotchi {
@Id
private UUID id;
private String name;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "pocket_id")
private Pocket pocket;
@Enumerated(STRING)
private Status status;
public static DeletedTamagotchi newDeletedTamagotchi(Tamagotchi tamagotchi) {
DeletedTamagotchi deletedTamagotchi = new DeletedTamagotchi();
deletedTamagotchi.setId(UUID.randomUUID());
deletedTamagotchi.setName(tamagotchi.getName());
deletedTamagotchi.setPocket(tamagotchi.getPocket());
deletedTamagotchi.setStatus(tamagotchi.getStatus());
return deletedTamagotchi;
}
}
Tamagotchi
entity is rather simple, so DeletedTamagotchi
contains the same fields. However, if the original entity were more complicated, it couldn’t be the case. For example, you could save the state of Tamagotchi
in Map<String, Object>
fields that transforms to JSONB in the database.
Also, DeletedTamagotchi
entity is package-private like Tamagotchi
. So, the presence of this entity is an implementation detail. The other parts of the code don’t need to know this and interact with DeletedTamagotchi
directly. Instead, it’s better to provide a single method Pocket.restoreTamagotchi
without additional details.
Now let's alter Pocket
entity to the new requirements. Look at the code example below:
@Entity
@NoArgsConstructor(access = PROTECTED)
public class Pocket {
/* fields and other methods */
@OneToMany(mappedBy = "pocket", cascade = PERSIST, orphanRemoval = true)
private List<DeletedTamagotchi> deletedTamagotchis = new ArrayList<>();
public void deleteTamagotchi(UUID tamagotchiId) {
Tamagotchi tamagotchi = tamagotchiById(tamagotchiId);
if (tamagothis.size() == 1) {
throw new TamagotchiDeleteException("Cannot delete Tamagotchi because it's the single one");
}
tamagotchis.remove(tamagotchi);
addDeletedTamagotchi(tamagotchi);
}
private void addDeletedTamagotchi(Tamagotchi tamagotchi) {
Iterator<DeletedTamagotchi> iterator = deletedTamagotchis.iterator();
// if Tamagotchi with the same has been deleted,
// remove information about it
while (iterator.hasNext()) {
DeletedTamagotchi deletedTamagotchi = iterator.next();
if (deletedTamagotchi.getName().equals(tamagotchi.getName()) {
iterator.remove();
break;
}
}
deletedTamagotchis.add(
newDeletedTamagotchi(tamagotchi)
);
}
public UUID restoreTamagotchi(String name) {
DeletedTamagotchi deletedTamagotchi = deletedTamagotchiByName(name);
return createTamagotchi(new TamagotchiCreateRequest(deletedTamagotchi.getName()));
}
}
The deleteTamagotchi
method also creates or replaces a DeletedTamagotchi
record. Meaning that every other code that calls the method for whatever reason doesn't violate the new requirement about soft deletion because it's been implemented internally.
To perform the required business operation, you should just invoke Pocket.restoreTamagotchi
. We hid all the complex details behind the scenes. What’s even better is that DeletedTamagotchi
is not a part of public API. Meaning that it can be easily modified or even removed, if it’s not needed anymore.
As you can see, placing business logic within an aggregate has significant benefits. However, it's not the end of the story. There are still some concerns we need to deal with. And the next one is querying data.
Querying data
When we deal with Hibernate, usually we use public getters to transform entity into DTO and return it to the user. However, only Pocket
entity is public now, and it doesn’t provide any getters (aside from Pocket.getId()
). How do we perform queries in this case? I can suggest several approaches.
Manual queries
The obvious solution is just writing regular JPQL or SQL statements. Hibernate uses reflection and doesn’t demand public getters for fields. This may work if you start a project from scratch. But if you already relying on getters to retrieve information from the entity and put it into DTO, then transition might be overwhelming. That’s why we have a second option.
Introducing toDto method
An entity can provide toDto
or similar method that returns its internal representation as a separate data structure. It’s similar to Memento design pattern. Look at the code example below:
@Entity
@NoArgsConstructor(access = PROTECTED)
@Setter(PRIVATE)
public class Pocket {
/* other fields and methods */
public PocketDto toDto() {
return new PocketDto(
id,
name,
tamagotchis.stream()
.map(Tamagotchi::toDto)
.toList()
);
}
}
@Entity
@NoArgsConstructor(access = PROTECTED)
@Setter(PRIVATE)
@Getter
class Tamagotchi {
/* other fields and methods */
public TamagotchiDto toDto() {
return new TamagotchiDto(id, name, status);
}
}
The returned DTO is an immutable object that couldn’t affect entities’ state. Besides, the approach is also helpful for unit testing. Let’s move on to this part.
Unit testing entities
We're going to test these scenarios:
-
Pocket
must always possess at least oneTamagotchi
. - If you delete
Tamagotchi
, you can restore it by name. - If you delete multiple
Tamagotchi
with the same name, you can only restore the last one.
The entire test suite is available by this link.
Pocket must always possess at least one Tamagotchi
Look at the unit tests below.
class PocketTest {
@Test
void shouldCreatePocketWithTamagotchi() {
Pocket pocket = Pocket.newPocket("My pocket");
PocketDto dto = pocket.toDto();
assertEquals(1, dto.tamagotchis().size());
}
@Test
void shouldForbidDeletionOfASingleTamagotchi() {
Pocket pocket = Pocket.newPocket("My pocket");
PocketDto dto = pocket.toDto();
UUID tamagotchiId = dto.tamagotchis().get(0).id();
assertThrows(
TamagotchiDeleteException.class,
() -> pocket.deleteTamagotchi(tamagotchiId)
);
}
}
The first one checks that Pocket
is being created with a single Tamagotchi
. Whilst the second one validates that you cannot delete Tamagotchi
if it’s a single one.
What I like about those tests is that they are unit ones. No database, no Testcontainers, just regular JUnit and we've validated business logic successfully. Cool! Let's move forward.
If you delete Tamagotchi, you can restore it by name
This one is a bit more complicated. Look at the code example below.
class PocketTest {
@Test
void shouldDeleteTamagotchiById() {
Pocket pocket = Pocket.newPocket("My pocket");
UUID tamagotchiId = pocket.createTamagotchi(new TamagotchiCreateRequest("My tamagotchi", CREATED));
pocket.deleteTamagotchi(tamagotchiId);
PocketDto dto = pocket.toDto();
assertThat(dto.tamagotchis())
.noneMatch(t -> t.name().equals("My tamagotchi"));
}
@Test
void shouldRestoreTamagotchiById() {
Pocket pocket = Pocket.newPocket("My pocket");
UUID tamagotchiId = pocket.createTamagotchi(new TamagotchiCreateRequest("My tamagotchi", CREATED));
pocket.deleteTamagotchi(tamagotchiId);
pocket.restoreTamagotchi("My tamagotchi");
PocketDto dto = pocket.toDto();
assertThat(dto.tamagotchis())
.anyMatch(t -> t.name().equals("My tamagotchi"));
}
}
The shouldDeleteTamagotchiById
checks that deletion works as expected. The other one validates restoreTamagotchi
method behaviour.
If you delete multiple Tamagotchi with the same name, you can only restore the last one
This one is the most challenging. Look at the code example below.
class PocketTest {
@Test
void shouldRestoreTheLastTamagotchi() {
Pocket pocket = Pocket.newPocket("My pocket");
UUID tamagotchiId = pocket.createTamagotchi(new TamagotchiCreateRequest("My tamagotchi", CREATED));
pocket.deleteTamagotchi(tamagotchiId);
tamagotchiId = pocket.createTamagotchi(new TamagotchiCreateRequest("My tamagotchi", PENDING));
pocket.deleteTamagotchi(tamagotchiId);
pocket.restoreTamagotchi("My tamagotchi");
PocketDto dto = pocket.toDto();
assertThat(dto.tamagotchis())
.anyMatch(t ->
t.name().equals("My tamagotchi")
&& t.status().equals(PENDING)
);
}
}
Here we do these steps:
- Create
Pocket
. - Create
Tamagotchi
with name ofMy tamagotchi
and statusCREATED
. - Delete
Tamagotchi
. - Create
Tamagotchi
with name ofMy tamagotchi
and statusPENDING
. - Restore
Tamagotchi
by nameMy tamagotchi
. - Verify that the last one
Tamagotchi
has been restored (with status ofPENDING
).
Here are tests' run result:
Rich Domain Model pattern allows us to test complex business scenarios with simple unit tests. I think it’s outstanding. However, integration tests are also important because we need to store data in DB not in RAM. Let’s discuss this part of the equation.
Integration testing
We use entities with a conjunction of repositories (Spring Data ones usually). Let’s write some use cases and test them:
- Create
Pocket
. - Create
Tamagotchi
. - Update
Tamagotchi
.
The entire test suite is available by this link.
Create Pocket
Look at the service example below:
@Service
@RequiredArgsConstructor
public class PocketService {
private final EntityManager em;
@Transactional
public UUID createPocket(String name) {
Pocket pocket = Pocket.newPocket(name);
em.persist(pocket);
return pocket.getId();
}
}
Time to write some integration tests. Look at the code snippet below:
@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
@Transactional(propagation = NOT_SUPPORTED)
@Import(PocketService.class)
class PocketServiceIntegrationTest {
@Container
@ServiceConnection
public static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:13");
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private TestEntityManager em;
@Autowired
private PocketService pocketService;
@BeforeEach
void cleanDatabase() {
// there is cascade constraint in the database deleting tamagotchis and deleted_tamagotchis
transactionTemplate.executeWithoutResult(
s -> em.getEntityManager().createQuery("DELETE FROM Pocket ").executeUpdate()
);
}
@Test
void shouldCreateNewPocket() {
UUID pocketId = pocketService.createPocket("New pocket");
PocketDto dto = transactionTemplate.execute(
s -> em.find(Pocket.class, pocketId).toDto()
);
assertEquals("New pocket", dto.name());
}
}
I use Testcontainers library to start PosgtreSQL in Docker. Flyway migration tool creates tables before tests run.
You can check out the migrations by this link.
I guess this snippet is not that complicated. So, let's go next.
Create Tamagotchi
Look at the code service implementation below:
@Service
@RequiredArgsConstructor
public class PocketService {
/* other fields and methods */
@Transactional
public UUID createTamagotchi(UUID pocketId, TamagotchiCreateRequest request) {
Pocket pocket = em.find(Pocket.class, pocketId);
return pocket.createTamagotchi(request);
}
}
As you can see, the Rich Domain Model pattern demands to declare services as thin layer that are easy to understand and test. And here is the test itself:
/* same Java annotations */
class PocketServiceIntegrationTest {
/* initialization... */
@Test
void shouldCreateTamagotchi() {
UUID pocketId = pocketService.createPocket("New pocket");
UUID tamagotchiId = pocketService.createTamagotchi(
pocketId,
new TamagotchiCreateRequest("my tamagotchi", CREATED)
);
PocketDto dto = transactionTemplate.execute(
s -> em.find(Pocket.class, pocketId).toDto()
);
assertThat(dto.tamagotchis())
.anyMatch(t ->
t.name().equals("my tamagotchi")
&& t.status().equals(CREATED)
&& t.id().equals(tamagotchiId)
);
}
}
This one is a bit more interesting. Firstly, we create a Pocket
and then add a Tamogotchi
inside it. Assertions checks that expected Tamagotchi
is present in result DTO.
Update Tamagotchi
This one is the most intriguing. Check out the implementation below:
@Service
@RequiredArgsConstructor
public class PocketService {
/* other fields and methods */
@Transactional
public void updateTamagotchi(UUID tamagotchiId, TamagotchiUpdateRequest request) {
UUID pocketId = em.createQuery(
"SELECT t.pocket.id AS id FROM Tamagotchi t WHERE t.id = :tamagotchiId",
UUID.class
)
.setParameter("tamagotchiId", tamagotchiId)
.getSingleResult();
Pocket pocket = em.find(Pocket.class, pocketId);
pocket.updateTamagotchi(tamagotchiId, request);
}
}
API demands to pass tamagotchiId
. But the domain model allows us to update Tamagotchi
only through Pocket
because the latter is the aggregate root. So, we determine pocketId
with additional query to DB and then select Pocket
aggregate by its id. Test is also quite interesting:
/* same Java annotations */
class PocketServiceIntegrationTest {
/* other fields and methods */
@Test
void shouldUpdateTamagotchi() {
UUID pocketId = pocketService.createPocket("New pocket");
UUID tamagotchiId = pocketService.createTamagotchi(
pocketId,
new TamagotchiCreateRequest("my tamagotchi", CREATED)
);
pocketService.updateTamagotchi(
tamagotchiId,
new TamagotchiUpdateRequest("another tamagotchi", PENDING)
);
PocketDto dto = transactionTemplate.execute(
s -> em.find(Pocket.class, pocketId).toDto()
);
assertThat(dto.tamagotchis())
.anyMatch(t ->
t.name().equals("another tamagotchi")
&& t.status().equals(PENDING)
&& t.id().equals(tamagotchiId)
);
}
}
The steps are:
- Create
Pocket
. - Create
Tamagotchi
. - Update
Tamagotchi
. - Validate the result DTO.
Here is the execution result for all integration tests:
Nothing complicated, don't you think?
Performance implications
Rich Domain Model brings overhead for sure. However, there are
some workarounds to reach compromise.
Optimization of queries
Firstly, let's have a look at PocketService.updateTamagotchi
method again:
@Service
@RequiredArgsConstructor
public class PocketService {
/* other fields and methods */
@Transactional
public void updateTamagotchi(UUID tamagotchiId, TamagotchiUpdateRequest request) {
UUID pocketId = em.createQuery(
"SELECT t.pocket.id AS id FROM Tamagotchi t WHERE t.id = :tamagotchiId",
UUID.class
)
.setParameter("tamagotchiId", tamagotchiId)
.getSingleResult();
Pocket pocket = em.find(Pocket.class, pocketId);
pocket.updateTamagotchi(tamagotchiId, request);
}
}
The problem is that we retrieve all existing Tamagotchi
instances for a specified Pocket
when we actually want to update a single one. Look at the log below:
select t1_0.pocket_id from tamagotchi t1_0 where t1_0.id=?
select p1_0.id,p1_0.name from pocket p1_0 where p1_0.id=?
select t1_0.pocket_id,t1_0.id,t1_0.name,t1_0.status
from tamagotchi t1_0 where t1_0.pocket_id=?
We can change queries to restrict the transmission of unnecessary data. Look at the code example below:
@Service
@RequiredArgsConstructor
public class PocketService {
/* other fields and methods */
@Transactional
public void updateTamagotchi(UUID tamagotchiId, TamagotchiUpdateRequest request) {
Pocket pocket = em.createQuery(
"""
SELECT p FROM Pocket p
LEFT JOIN FETCH p.tamagotchis t
WHERE t.id = :tamagotchiId
""",
Pocket.class
).setParameter("tamagotchiId", tamagotchiId)
.getSingleResult();
pocket.updateTamagotchi(tamagotchiId, request);
}
}
Instead of selecting all existing Tamagotchi
instances for the specified Pocket
, we retrieve Pocket
and the only associated Tamagotchi
instance by specified id. Log also looks differently:
select
p1_0.id,
p1_0.name,
t1_0.pocket_id,
t1_0.id,
t1_0.name,
t1_0.status
from pocket p1_0
left join tamagotchi t1_0 on p1_0.id=t1_0.pocket_id
where t1_0.id=?
Even if Pocket
contains thousands of Tamagotchi
, it won’t affect the performance of the application. Because it will retrieve only a single one. If you run test cases from the previous paragraph, they will also pass successfully.
Pinpointing optimized checks
Nevertheless, the previous technique has limitations. To understand this, let's write another test. As we've already discussed, the business rule demands that each Tamagotchi
must have a unique name within Pocket
. Let's test this behaviour. Look at the code snippet below:
@Test
void shouldUpdateTamagotchiIfThereAreMultipleOnes() {
UUID pocketId = pocketService.createPocket("New pocket");
UUID tamagotchiId = pocketService.createTamagotchi(
pocketId,
new TamagotchiCreateRequest("Cat", CREATED)
);
pocketService.createTamagotchi(
pocketId,
new TamagotchiCreateRequest("Dog", CREATED)
);
assertThrows(
TamagotchiNameInvalidException.class,
() -> pocketService.updateTamagotchi(tamagotchiId, new TamagotchiUpdateRequest("Dog", CREATED))
);
}
There are two Tamagotchi
with names of Cat
and Dog
. We try to rename Cat
to Dog
. Here, we expect to get TamagotchiNameInvalidException
. Because business rule should validate this scenario. But if you run the test, you’ll get this result:
Expected com.example.demo.domain.exception.TamagotchiNameInvalidException to be thrown, but nothing was thrown.
Why is that? Look again at Pocket.updateTamagotchi
method declaration:
public void updateTamagotchi(UUID tamagotchiId, TamagotchiUpdateRequest request) {
Tamagotchi tamagotchi = tamagotchiById(tamagotchiId);
tamagotchi.changeName(request.name());
tamagotchi.changeStatus(request.status());
validateTamagotchiNamesUniqueness();
}
private void validateTamagotchiNamesUniqueness() {
Set<String> names = new HashSet<>();
for (Tamagotchi tamagotchi : tamagotchis) {
if (!names.add(tamagotchi.getName())) {
throw new TamagotchiNameInvalidException(
"Tamagotchi name is not unique: " + tamagotchi.getName());
}
}
}
As you can see, Pocket
aggregate expects to have access for all Tamagotchi
to validate the business rule. But we changed the query to select only a single Tamagotchi
(the one we want to update). That’s why the exception is not raised. Because there is always a single Tamagotchi
on the list and we cannot violate the uniqueness.
I see people trying to remove such validations from an aggregate. But I think you shouldn't do that. Instead, it's better to perform another optimized check in the service level in advance. To understand this approach, look at the schema below:
Aggregate should always be valid. You can’t predict all likely future outcomes. Maybe you’ll call Pocket
in another scenario. So, if you drop a check from an aggregate completely, you may accidentally violate business rule.
Nevertheless, we live in a real world where performance matters. It’s much better to execute a single exists
SQL statement then retrieve all Tamagotchi
instances from the database. So, you put optimized check specifically where it’s needed. But you also leave the aggregate untouched.
Look at the final code snippet of PocketService.updateTamagotchi
method:
@Transactional
public void updateTamagotchi(UUID tamagotchiId, TamagotchiUpdateRequest request) {
boolean nameIsNotUnique = em.createQuery(
"""
SELECT COUNT(t) > 0 FROM Tamagotchi t
WHERE t.id <> :tamagotchiId AND t.name = :newName
""",
boolean.class
).setParameter("tamagotchiId", tamagotchiId)
.setParameter("newName", request.name())
.getSingleResult();
if (nameIsNotUnique) {
throw new TamagotchiNameInvalidException("Tamagotchi name is not unique: " + request.name());
}
UUID pocketId = em.createQuery(
"SELECT t.pocket.id AS id FROM Tamagotchi t WHERE t.id = :tamagotchiId",
UUID.class
)
.setParameter("tamagotchiId", tamagotchiId)
.getSingleResult();
Pocket pocket = em.find(Pocket.class, pocketId);
pocket.updateTamagotchi(tamagotchiId, request);
}
Firstly, we check that any other Tamagotchi
(aside from the one we want to update) already has the same name. If that’s true, we throw an exception. If you run the previous test again and check log, you’ll see that only COUNT
query has been invoked:
select count(t1_0.id)>0
from tamagotchi t1_0
where t1_0.id!=? and t1_0.name=?
Anyway, I don't recommend you to overuse this approach. You should treat it like an accurately pinned patch. In other words, put it only where it's needed. Otherwise, I'd prefer relying on domain logic and leave code in services as simple as possible.
Database generated ID
Previously I’ve mentioned that I’ll show you examples of client generated ID. However, sometimes we want to use other ID types. For example, sequence generated ones. Is this Rich Domain Model pattern also applicable to these ID types? It is, but there are also some concerns.
Firstly, have a look at Pocket
and Tamagotchi
entities using IDENTITY generation strategy:
@Entity
@NoArgsConstructor(access = PROTECTED)
@Setter(PRIVATE)
public class Pocket {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
/* other fields aren't important */
public Long createTamagotchi(TamagotchiCreateRequest request) {
Tamagotchi newTamagotchi = Tamagotchi.newTamagotchi(request.name(), request.status(), this);
tamagotchis.add(newTamagotchi);
validateTamagotchiNamesUniqueness();
// always returns null
return newTamagotchi.getId();
}
/* other methods aren't important */
public static Pocket newPocket(String name) {
Pocket pocket = new Pocket();
pocket.setName(name);
pocket.createTamagotchi(new TamagotchiCreateRequest("Default", CREATED));
return pocket;
}
}
@Entity
@NoArgsConstructor(access = PROTECTED)
@Setter(PRIVATE)
@Getter
class Tamagotchi {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
/* other fields and methods aren't important */
public static Tamagotchi newTamagotchi(String name, Status status, Pocket pocket) {
Tamagotchi tamagotchi = new Tamagotchi();
tamagotchi.setName(name);
tamagotchi.setPocket(pocket);
tamagotchi.setStatus(status);
return tamagotchi;
}
}
As you can see, we don’t assign ID directly anymore. Instead, we leave the field with null
value and let Hibernate fill it later. Unfortunately, this decision breaks the logic of Pocket.createTamagotchi
method. We do not set ID during the creation of Tamagotchi
object. So, the invocation of Tamagotchi.getId
always returns null
(until you flush changes to the database).
There are several ways to fix this issue.
Manually fill the id
You can eliminate @GeneratedValue
annotation usage and pass the ID value directly in the constructor. In this case, you have to invoke SELECT nextval('mysequence')
statement and pass its result to an entity. Look at the code example below:
@Entity
@NoArgsConstructor(access = PROTECTED)
@Setter(PRIVATE)
public class Pocket {
@Id
private Long id;
/* other fields aren't important */
public Long createTamagotchi(long tamagotchiId, TamagotchiCreateRequest request) {
Tamagotchi newTamagotchi = Tamagotchi.newTamagotchi(tamagotchiId, request.name(), request.status(), this);
tamagotchis.add(newTamagotchi);
validateTamagotchiNamesUniqueness();
// always returns null
return newTamagotchi.getId();
}
/* other methods aren't important */
public static Pocket newPocket(long id, String name) {
Pocket pocket = new Pocket();
pocket.setId(id);
pocket.setName(name);
pocket.createTamagotchi(new TamagotchiCreateRequest("Default", CREATED));
return pocket;
}
}
@Entity
@NoArgsConstructor(access = PROTECTED)
@Setter(PRIVATE)
@Getter
class Tamagotchi {
@Id
private Long id;
/* other fields and methods aren't important */
public static Tamagotchi newTamagotchi(long id, String name, Status status, Pocket pocket) {
Tamagotchi tamagotchi = new Tamagotchi();
tamagotchi.setId(id);
tamagotchi.setName(name);
tamagotchi.setPocket(pocket);
tamagotchi.setStatus(status);
return tamagotchi;
}
}
The advantage is that your entity classes do not depend on some Hibernate magic and you can still validate business cases with regular unit tests. But you also make your code more verbose. Because you to have pass IDs manually.
Anyway, this approach is worth considering.
I found this option in this article. Actually, the author demands to stop use Hibernate at all. Even though I like Hibernate, I found some arguments intriguing.
Introducing business key
Sometimes passing IDs manually is nearly impossible. Maybe it requires too much refactoring that is unbearable. Or maybe your application works with MySQL which doesn't support sequences but only auto increment columns.
Though you can emulate sequences in MySQL by creating a regular table, this approach is not well performant.
In this case, you can introduce business key. That is a separate value that can identify an entity uniquely. Though it doesn’t mean that the business key must be globally unique. For example, if you point to Tamagotchi
by name
and it’s only unique within a Pocket
, then you can identify Tamagotchi
by a combination of (pocket_business_key, tamagothic_name)
.
Nevertheless, each business key should be unmodifiable. Otherwise, you can compromise the identity of your entities. So, pay good attention to this point.
Also, a good example of a business key is a slug. Look at the URL of this article. Do you see that it contains its name and some hash value? That is the slug. It assigns only once when the article is created but never changes (even if I change the article’s name). So, if your entities don’t have an obvious candidate for business key, introducing a slug might be an option.
Is Rich Domain Model always worth it?
There is no ultimate decision in software development. Every approach is just a compromise. Rich Domain Model pattern is no exception.
I started my article by explaining the problems of the Anemic Domain Model to you. They all valid and make sense. But it doesn’t mean that the Rich Domain model has no disadvantages. I can think of these:
- If you work with Hibernate, then the Rich Domain Model pattern is not so popular. It’s just the reality. There are dozens of articles on the Internet with Hibernate examples and total absence of the Rich Domain Model. People got used to the Anemic Domain Model and you have to take it into account.
- Rich Domain Model pattern may also bring some performance penalties. Some of them can be easily fixed. But others might become a headache. If your application is supposed to be high loaded, you have to make sure that invariants’ check won’t slow the response time too much.
- Rich Domain Model usage often leads to god object entities. Of course, it makes maintenance harder. There are ways to fix that. For example, Vaughn Vernon wrote 3 articles about effective aggregate design. However, if your entity is already a god object, it’s will be tough to refactor it.
Conclusion
In the end, I can say that I think that the Rich Domain Model acts better than the Anemic one. But don’t apply it blindly. You should also consider possible consequences and make decisions wisely.
Thank you very much for reading this long piece. I hope you've learnt something new. If you found it interesting, please share it with your friends and colleagues, press the like button, and leave your comments down below. I'll be glad to hear your opinions and discuss questions. Have a nice day!
Resources
- Hibernate
- Stop Using JPA/Hibernate
- Anemic Domain Model pattern
- Status quo
- Rich Domain Model pattern
- The entire repository with code examples
- Open-Closed principle
- Validation VS invariants
- Saga pattern
- Encapsulation (computer programming)
- Lombok library
- @Transactional annotation
- Spaghetti code
- Mockito library
- Tescontainers library
- Flaky tests
- My article about Unit Testing
- Tactical DDD
- DDD Aggregate
- Why does JPA require No-Args Constructor for Domain Objects
- What does it mean to write a buggy code
- Are soft deletes a good idea?
- PostgreSQL, Datatype JSON
- JPQL
- Memento design pattern
- Flyway migration tool
- PostgreSQL, create sequences
- Hibernate, IDENTITY generation strategy
- Auto increment columns in MySQL
- Surrogate key VS natural key differences
- God object
- Effective aggregate design by Vaughn Vernon
- JPA flush
Top comments (10)
Nice explanation about the theme, but considering tradeoffs of rich domain with hibernate orm, don´t be better goes to a concentric archictecture(clean arch, hexagonal, etc) directly?
Sounds like a lack of single responsability, mixing domain rules with persistent data operations.
@kauan_amarante In my point of view, Rich Domain Model with Hibernate is the same thing as hexagonal architecture. Consider such Hibernate entity:
As you can see,
Pocket
entity is a regular Java class. There are no persistent data operations. You can verifyPocket
operations with simple unit tests.Annotations are just hints but not executable code. So, Hibernate treats them as something that should be persisted. But this implementation is hidden behind the scenes.
If you try to go for canonical hexagonal architecture, you will have two options:
Implement your own Hibernate-like framework
Suppose that
Pocket
is a simple Java class with no Hibernate annotations. Look at the code snippet below:There are no much difference from Hibernate entity (except that this class has only all-args constructor). At the same time, you still have to translate entity changes to the database. Therefore, you would have to implement your own dirty checking mechanism. And this is a really complicated task.
I understand that Hibernate's dirty checking algorithm can be not so efficient. But it's generic for each scenario. You can write something that suits your requirements better. However, such infrastructure code will increase the complexity of your application severely. And most likely you would want to move this logic to a separate project (i.e. create your own Hibernate).
Create separate domain and Hibernate classes.
Theoretically you could have two classes
Pocket
andPocketEntity
. The first one is the domain class (look at the example in the previous paragraph). The second one is the Hibernate entity. In this case, persistence layer interacts withPocketEntity
and the business logic touches onlyPocket
.I've seen some examples on the Internet. But to be honest, I don't see any value with this approach.
Pocket
andPocketEntity
are most likely identical. So, you would need code which responsibilty is dumb mappingPocketEntity
toPocket
and vice-versa. Why would you need this if you can just interact with singlePocket
directly?I understand your point of view, it is all about "Create separate domain and Hibernate classes." and I think your domain is a little bit "simple" yet, database persistence entity and domain entity are different things, one cannot depends each other. A simple example is: for your database record you need something that identifies(like PK) but for your domain entity it's irrelevant, meaning the PK exists only in database context.
I think that way you can achieve the single responsibility principle and increase manutenability, even that is only hibernate "annotations" it's different concerns.
An entity describes its behaviour by public methods but not the fields. If you need database PK which is not part of the contract, you can make it a private field and don't expose it with a getter. Hibernate can work with private fields that have no getters or setters.
don't really look like a hexagonal architecture
more info
reflectoring.io/spring-hexagonal/
anyway, with microservice way, anemic seem the way to go
with mapstruct, spring data, lombok a lot of code showed in this article don't need to be wrote
Amazing article, thanks! I could really learn a lot from it
Some points I'd also think is worth mentioning:
Chapter: Don't add setters, getters, and public no-args constructor -> Don't add LOMBOK setters, getters, and public no-args constructor
Creating setters and getters (even lombok getters and setters) is generally fine if the set / get of the attribute has no rules associated with it (yet) - You can override it later on, I kind of disagree with the
changeXXX
method approach and would do the validation inside the public setter itself, but it's really more secure than a default setter without the business logic, and I think that's the main point in here.Another point of getters: Be careful when "getting" a list, make sure you give the user a copy of the list, not the list itself, otherwise, the person will be able to avoid business rules set on the
setList
method.Also, be careful when adding builders, by standard, lombok builders will not protect your entity, however, there are some alternatives:
You can make sure your
.builder()
method that provides you anentityBuilder
receives as parameters the required fields, or uselombok.NonNull
, which I personally don't like, since it's a runtime check.Thank you for such meticulous and thorough research. I have a question.
The code from the "performance implication section" heavily relies on the encapsulated implementation details from the
Pocket
aggregate. The service knows precisely what needs to be prefetched.We still preserve the invariant, but this solution is arguably more cognitively complex than the option to give up on implementing the aggregate and preserving the invariant within the boundaries of a service method instead.
I mean, it's a tradeoff. Aggregates tend to cause performance issues, so it's easy to use them when it's possible to fetch them eagerly into the memory without even bothering with lazy-load scenarios. Using a service seems less of a hustle if I can't do it.
@maxarshinov
The short answer is 'it depends'.
There are no ultimate solutions in software engineering. Of course, you can put some logic within a service and remove OneToMany relation at all. But that would make your model less rich and more anemic.
The idea of Rich Domain Model is to make the model as solid as possible. Therefore, the service layer contains no business logic (or few). On the contrary, the Anemic Domain Model advocates to declare entities as dummy DTOs and put all business operations inside service layer.
In my opinion, Rich Domain Model is easier to read and understand (btw, my colleagues also agree with me). But I'm just a software engineer and not a genius :) So, this article describes my opinion upon this topic and nothing more.
I can say that putting the logic inside the service layer has no harm by itself. If you do something on purpose (and you know why), then you're good to go. The major criteria of any software product is maintainability. If your way makes it better, then it's a good pattern in your case. But it also can be a poor solution in other teams. So, yeah, it depends.
Thanks for the articles, I started reading from the end and missed something. It is correctly noted that the rich model is rarely used in real projects; mostly I see a transactional script. And often developers do not understand how and why to use Hibernate, especially the relationships between entities and this is a pain.
I also would like to