Posted on Sep 7 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: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:You can check out the entire repository with code examples by this link.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.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.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.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.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: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: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.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:Let's start our journey with refactoring Pocket and Tamagotchi to Rich Domain Model.Firstly, look at the initial approach of designing Pocket and Tamagotchi entities following Anemic Domain Model: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?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: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.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: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: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.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: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.We've already discussed three points:If we remove those pieces, the code will look like this: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: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.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.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: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.To implement this requirement, we need to alter createTamagotchi and updateTamagotchi methods a bit. Look at the code example below:You’ve probably noticed that I added a getter for Tamagotchi.name field. Because Pocket and Tamagotchi form a single aggregate, it’s fine to provide getters. Because Pocket might need this information. However, Tamagotchi should not request anything from Pocket. It’s also better to mark this getter as package-private. So, no one can access it outside of the package.I understand that validateTamagotchiNamesUniqueness doesn't perform well. Don't worry, we'll discuss workarounds later in the Performance 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.This one is tricky and involves soft deletion. It also has additional points: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.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