Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Rich Domain Model with Hibernate

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 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: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.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.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.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: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.We're going to test these scenarios:The entire test suite is available by this link.Look at the unit tests below.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.This one is a bit more complicated. Look at the code example below.The shouldDeleteTamagotchiById checks that deletion works as expected. The other one validates restoreTamagotchi method behaviour.This one is the most challenging. Look at the code example below.Here we do these steps: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.We use entities with a conjunction of repositories (Spring Data ones usually). Let’s write some use cases and test them:The entire test suite is available by this link.Look at the service example below:Time to write some integration tests. Look at the code snippet below: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.Look at the code service implementation below: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: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.This one is the most intriguing. Check out the implementation below: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:The steps are:Here is the execution result for all integration tests:Nothing complicated, don't you think?Rich Domain Model brings overhead for sure. However, there are some workarounds to reach compromise.Firstly, let's have a look at PocketService.updateTamagotchi method again: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:We can change queries to restrict the transmission of unnecessary data. Look at the code example below: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: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.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: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:Why is that? Look again at Pocket.updateTamagotchi method declaration: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: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: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.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: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.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: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.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.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: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!Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well Confirm For further actions, you may consider blocking this person and/or reporting abuse Arpan Bandyopadhyay - Jun 12 Eduard Dyckman - Jun 10 Petr Filaretov - May 29 Yoshio Terada - Jun 9 Once suspended, kirekov will not be able to comment or publish posts until their suspension is removed. Once unsuspended, kirekov will be able to comment and publish posts again. Once unpublished, all posts by kirekov will become hidden and only accessible to themselves. If kirekov is not suspended, they can still re-publish their posts from their dashboard. Note: Once unpublished, this post will become invisible to the public and only accessible to Semyon Kirekov. They can still re-publish the post if they are not suspended. Thanks for keeping DEV Community safe. Here is what you can do to flag kirekov: kirekov consistently posts content that violates DEV Community's code of conduct because it is harassing, offensive or spammy. Unflagging kirekov will restore default visibility to their posts. DEV Community — A constructive and inclusive social network for software developers. With you every step of your journey. Built on Forem — the open source software that powers DEV and other inclusive communities.Made with love and Ruby on Rails. DEV Community © 2016 - 2023. We're a place where coders share, stay up-to-date and grow their careers.



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

Rich Domain Model with Hibernate

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×