DEV Community

Rich Domain Model with Spring Boot and Hibernate

Semyon Kirekov on September 07, 2023

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...
Collapse
 
kauanmocelin profile image
Kauan Mocelin

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.

Collapse
 
kirekov profile image
Semyon Kirekov • Edited

@kauan_amarante In my point of view, Rich Domain Model with Hibernate is the same thing as hexagonal architecture. Consider such Hibernate entity:

@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;
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, Pocket entity is a regular Java class. There are no persistent data operations. You can verify Pocket 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:

  1. Implement your own Hibernate-like framework.
  2. Create separate domain and Hibernate classes.

Implement your own Hibernate-like framework

Suppose that Pocket is a simple Java class with no Hibernate annotations. Look at the code snippet below:

@Setter(PRIVATE)
@AllArgsConstructor
public class Pocket {
  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 */
}
Enter fullscreen mode Exit fullscreen mode

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 and PocketEntity. 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 with PocketEntity and the business logic touches only Pocket.

I've seen some examples on the Internet. But to be honest, I don't see any value with this approach. Pocket and PocketEntity are most likely identical. So, you would need code which responsibilty is dumb mapping PocketEntity to Pocket and vice-versa. Why would you need this if you can just interact with single Pocket directly?

Collapse
 
kauanmocelin profile image
Kauan Mocelin

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.

Thread Thread
 
kirekov profile image
Semyon Kirekov • Edited

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.

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.

Collapse
 
os2 profile image
Marc

don't really look like a hexagonal architecture

Image description

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

Collapse
 
kauegatto profile image
Kauê Gatto

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

  1. 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.

  2. 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.

  3. 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 an entityBuilder receives as parameters the required fields, or use lombok.NonNull, which I personally don't like, since it's a runtime check.

Collapse
 
rzenkov profile image
Roman

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.

Collapse
 
maxarshinov profile image
max-arshinov • Edited

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.

Collapse
 
kirekov profile image
Semyon Kirekov

@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.

Collapse
 
ygmarchi profile image
ygmarchi

I also would like to

  • inject other components in entities, so that they collaborate with those other components to implement a complex logic
  • have queries in entities. To me queries are part of the logic and I want to incapsulate them in my rich model
Collapse
 
kirekov profile image
Semyon Kirekov

@ygmarchi

inject other components in entities, so that they collaborate with those other components to implement a complex logic

I think it's better to use the strategy pattern here. For example, you can pass an interface as a parameter:

class Order {
    public void pay(PaymentGateway gateway) {
        // call gateway as part of business logic...
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, PaymentGateway is an interface. You can test the behavior with a mock or stub implementation.


have queries in entities. To me queries are part of the logic and I want to encapsulate them in my rich model

In my opinion, entities are part of business logic not the queries. Therefore, entities represent CREATE/UPDATE/DELETE operations. If you want to combine your entities as views, there are options:

  1. Return an entity from the service and convert it to DTO with some mapper
  2. Create dedicated queries to instantiate required DTOs