Anemic Domain Model vs Rich Domain Model in Java


08 Mar 2021  Michal Fabjanski  19 mins read.

When we write code, we try to hide as many details as possible - using appropriate access modifiers. Most often we use getters and setters methods to set the appropriate state of an object. Is this a good practice? In this article you will learn what is Anemic Domain Model and Rich Domain Model and which way of creating models is the best. What’s more, you’ll find a special opportunity from CodeGym at the end of this post. I invite you to read :)

Why are getters and setters unsafe? Anemic Domain Model vs Rich Domain Model

Take a look at the code below:

import java.util.UUID;  
  
public class User {  
 public UUID userId;  
 public String name;  
 public String email;  
 public boolean isBlocked;   
 public boolean isActive;
}

Do you see any potential risks? That’s right! The class contains public fields that can be changed anywhere else in the application code. It will be hard for us to control such an application, especially if it becomes a big project.

All right. Let’s make changes so that the fields are not public:

import java.util.UUID;

public class User {
    private UUID userId;
    private String name;
    private String email;
    private boolean isBlocked;
    private boolean isActive;

    public UUID getUserId() {
        return userId;
    }

    public void setUserId(UUID userId) {
        this.userId = userId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public boolean isBlocked() {
        return isBlocked;
    }

    public void setBlocked(boolean blocked) {
        isBlocked = blocked;
    }

    public boolean isActive() {
        return isActive;
    }

    public void setActive(boolean active) {
        isActive = active;
    }
}  

Does it look familiar? You have certainly come across such a code before. In accordance with best practices - fields are private. But adding getters and setters does not improve the situation (because we have the same access to variables - as if they were public). So how to deal with this problem? The answer is the Rich Domain Model you are about to read about.

Why is public unsafe?

Let’s go back to the first example.

public class User {  
 public UUID userId;  
 public String name;  
 public String email;  
 public boolean isBlocked;   
 public boolean isActive;
}
  

The User class above exposes its implementation, which in this case is very dangerous. Why? We can freely manipulate all the fields in this class. You can set any field without any consequences. Another problem can be changing the user id at any time in the code.

Anemic Domain Model in Java

A class with state exposed by getters and setters is an Anemic Model. An anemic domain model is often used as DTO (Data Transfer Object) object - a class with private fields and setters and getters. With an anemic approach, our models become just “containers” for the data, and all the logic for operating on that data is extracted to classes such as: **Service, **Helper, **Processor, etc.

What do you think the UserService class might look like? Let’s put together some requirements:

  • The user at creation is non-active. Only after email confirmation his account is activated,
  • By default, the user is not blocked. In case of breaking the rules, the user can be blocked by the administrator,
  • User email address must be from gmail.com domain,
  • User name must be shorter than 100 characters.

Here we go! Following the most popular pattern, we create individual services:

UserActivationService - service responsible for user activation. A user can only be activated once!

import anemic.exceptions.UserAlreadyActivatedException;
import anemic.exceptions.UserNotFoundException;
import anemic.model.User;
import anemic.repository.UserRepository;

import java.util.UUID;

public class UserActivationService {

    private final UserRepository userRepository;


    public UserActivationService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void activateUser(UUID userId) {
        var user = userRepository.getById(userId).orElseThrow(UserNotFoundException::new);
        validateIfUserIsActive(user);
        user.setActive(true);
        userRepository.save(user);
    }

    private void validateIfUserIsActive(User user) {
        if (user.isActive()) {
            throw new UserAlreadyActivatedException(user);
        }
    }

}

UserBlockerService - service responsible for blocking users. As with activation - a user can only be blocked once. It is not possible to block a previously blocked user.

import anemic.exceptions.UserAlreadyBlockedException;
import anemic.exceptions.UserNotFoundException;
import anemic.model.User;
import anemic.repository.UserRepository;

public class UserBlockerService {

    private final UserRepository userRepository;


    public UserBlockerService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void block(User user) {
        userRepository.getById(user.getUserId()).orElseThrow(UserNotFoundException::new);

        validateIfUserIsNotBlocked(user);
        user.setBlocked(true);

        userRepository.save(user);
    }

    private void validateIfUserIsNotBlocked(User user) {
        if (user.isBlocked()) {
            throw new UserAlreadyBlockedException(user);
        }
    }
}

UserRegistrationService - service responsible for user registration. According to the above requirements, we need to validate the username length and email address.

import anemic.exceptions.UserAlreadyExistsException;
import anemic.exceptions.UserNameTooLongException;
import anemic.exceptions.WrongEmailDomainException;
import anemic.model.User;
import anemic.repository.UserRepository;

public class UserRegistrationService {

    private static final String MAIL_DOMAIN = "gmail.com";

    private final UserRepository userRepository;

    public UserRegistrationService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void registerUser(User user) {
        if (userRepository.getById(user.getUserId()).isPresent()) {
            throw new UserAlreadyExistsException();
        }

        validateUserEmailDomain(user.getEmail());
        validateUserName(user.getName());
        user.setActive(false);
        user.setBlocked(false);
        userRepository.save(user);
    }

    private void validateUserEmailDomain(String email) {
        if (!email.endsWith(MAIL_DOMAIN)) {
            throw new WrongEmailDomainException(email);
        }
    }

    private void validateUserName(String name) {
        if (name.length() > 100) {
            throw new UserNameTooLongException(name);
        }
    }
}

So we have a simple, anemic User domain model that contains no logic. We also have 3 services (UserActivationService, UserBlockerService, UserRegistrationService) with all the logic of the user activation, blocking and registration. This solution is very popular but has a few cons:

  • It simply violates the “Tell, Don’t Ask” principle which states that objects should tell the client what they can or cannot do rather than exposing properties and leaving it up to the client to determine if an object is in a particular state for a given action to take place.
  • We often have to repeat code across multiple places. In the case of our project - in UserBlockerService and UserActivationService we had to repeat checking if the user already exists.
  • The model is completely untestable because we cannot ensure that the model doesn’t get invalid at some point. Theoretically, some other **Service could create a user with a disallowed domain in email and add it to the database - which would break one of our requirements.

Rich Domain Model in Java

Now let’s look at the same example in the rich domain approach. Let’s start with the email field. We can move it to a separate class - Email. This has the advantage that we can move the email domain validations to the Email object. This will prevent us from creating an incorrect email address (with a non-allowed domain). We will also not be able to modify the email after it has been created.

import static java.util.Objects.requireNonNull;

public class Email {

    private static final String MAIL_DOMAIN = "gmail.com";

    private final String email;

    public Email(String email) {
        requireNonNull(email, "Email not provided");
        checkDomain(email);
        this.email = email;
    }

    public static Email of(final String email) {
        return new Email(email);
    }

    private void checkDomain(String email) {
        if (!email.endsWith(MAIL_DOMAIN)) {
            throw new WrongEmailDomainException(email);
        }
    }
}

Now do the same for the Username field:

import static java.util.Objects.requireNonNull;

public class Username {

    private static final int MAX_USERNAME_LENGTH = 100;


    private final String username;

    public Username(String username) {
        requireNonNull(username, "username not provided");
        checkUsernameLength(username);
        this.username = username;
    }

    public static Username of(final String userName) {
        return new Username(userName);
    }

    private void checkUsernameLength(String username) {
        if (username.length() > MAX_USERNAME_LENGTH) {
            throw new UserNameTooLongException(username);
        }
    }
}

As you can see the classes are similar. They have validation and a static method to create an object (Static Factory Method). It is not possible to create an Email object with an invalid domain and a Username object with a too long name (as required). Below I will show you the code of a rich User domain:

import static com.google.common.base.Preconditions.checkState;

public class User {


    private UUID userId;
    private Username userName;
    private Email email;
    private boolean isBlocked;
    private boolean isActive;

    private User(UUID userId, Username userName, Email email, boolean isBlocked, boolean isActive) {
        this.userId = userId;
        this.userName = userName;
        this.email = email;
        this.isBlocked = isBlocked;
        this.isActive = isActive;
    }

    public static User create(UUID userId, Username userName, Email email) {
        var isBlocked = false;
        var isActive = false;
        return new User(userId, userName, email, isBlocked, isActive);
    }

    public void block() {
        checkState(canBeBlocked(), "User can not be blocked");
        this.isBlocked = true;
    }

    public void activate() {
        checkState(canBeActivated(), "User is already activated");
        this.isActive = true;
    }

    public boolean isBlocked() {
        return isBlocked;
    }

    public boolean isActive() {
        return isActive;
    }

    public UUID getUserId() {
        return userId;
    }

    public Username getUserName() {
        return userName;
    }

    private boolean canBeActivated() {
        return !isActive;
    }

    private boolean canBeBlocked() {
        return !isBlocked;
    }
}

In block() and activate() methods I used checkState() from Guava library. If the first parameter is false - we throw IllegalStateException with the content from the second parameter. Thanks to this we do not have to create additional “if” conditions.

As you can see, compared to the anemic model our rich domain model does not provide the ability to modify fields externally. There are no setter methods. Variables have private access, they are set in a private constructor. There is a public static factory method that creates a new User object. Additionally, we have some getter methods to be able to extract some information (such as username or userID).

The most important thing about the above code is that the rich domain object provides business methods, exposing the behavior of the model and not just its state (as for standard models with getters/setters).

The main advantage of the Rich Domain Model

A rich domain does not allow us to create invalid objects. You are not able to create a user with a wrong email address or too long username - as in the anemic domain model. The state of an object is always consistent, and consistency is guaranteed by the object itself, not by conditions checked in code that operates on objects (such as **Services classes).

If we use an anemic model - we make our object public, allow for any modification of the model variables and all the responsibility (logic) is transferred to external classes of the Service type. This creates a lot of IF conditions and a big risk that before performing some operation we forget to check some condition that would exclude the possibility of performing that operation.

If the logic is distributed among many services, and the model itself allows any modification of its internal data - we are very exposed not only to making a mistake, but also to overwriting the field value, unauthorized/uncontrolled access to the field. By moving the same logic into the domain model itself - there is less chance of error.

Example tests of a rich domain model

Testing the rich model is straightforward. Below you can see what a sample test looks like:

import anemic.exceptions.UserNameTooLongException;
import anemic.exceptions.WrongEmailDomainException;
import org.apache.commons.text.RandomStringGenerator;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import rich.model.Email;
import rich.model.User;
import rich.model.Username;


import static java.util.UUID.randomUUID;
import static org.assertj.core.api.Assertions.*;

public class UserTest {

    @Test
    @DisplayName("The user after creation should not be active")
    public void shouldNotBeActiveAfterCreation() {
        var user = User.create(randomUUID(), Username.of("John123"), Email.of("john@gmail.com"));
        assertThat(user.isActive()).isFalse();
    }

    @Test
    @DisplayName("The user can be blocked")
    public void canBeBlocked() {
        var user = User.create(randomUUID(), Username.of("John123"), Email.of("john@gmail.com"));
        assertThat(user.isBlocked()).isFalse();
        user.block();
        assertThat(user.isBlocked()).isTrue();
    }

    @Test
    @DisplayName("A blocked user cannot be blocked again")
    public void canBeBlockedOnlyOnce() {
        var user = User.create(randomUUID(), Username.of("John123"), Email.of("john@gmail.com"));
        user.block();
        assertThatCode(user::block).isInstanceOf(IllegalStateException.class);
    }

    @Test
    @DisplayName("The user can be activated")
    public void canBeActivated() {
        var user = User.create(randomUUID(), Username.of("John123"), Email.of("john@gmail.com"));
        assertThat(user.isActive()).isFalse();
        user.activate();
        assertThat(user.isActive()).isTrue();
    }

    @Test
    @DisplayName("The user cannot be activated again")
    public void canBeActivatedOnlyOnce() {
        var user = User.create(randomUUID(), Username.of("John123"), Email.of("john@gmail.com"));
        user.activate();
        assertThatCode(user::activate).isInstanceOf(IllegalStateException.class);
    }

    @Test
    @DisplayName("The email address must match the domain")
    public void emailMustMatchTheDomain() {
        assertThatCode(() -> User.create(randomUUID(), Username.of("John123"), 
        Email.of("john@wrongdomain.com"))).isInstanceOf(WrongEmailDomainException.class);
    }

    @Test
    @DisplayName("Username must have permission length")
    public void usernameMustHaveAllowedLength() {
        String longUsername = new RandomStringGenerator.Builder().build().generate(101);
        assertThatCode(() -> User.create(randomUUID(), Username.of(longUsername), 
        Email.of("john@wrongdomain.com"))).isInstanceOf(UserNameTooLongException.class);
    }
}

When to use getters and setters in Java objects?

You can safely use getters/setters in DTO (Data Transfer Object) objects. DTO models are used as entities when communicating with databases or as data transfer objects between REST services. Often setters are even necessary because libraries of various frameworks use them. But remember that it is best to map such objects to domain objects (which will use a rich model).

Special offer from CodeGym

CodeGym is an interactive educational platform where people can learn Java programming from scratch. I used this platform early in my career and it was great to solidify my knowledge of Java. It takes a very practical approach to learning programming. Also, it does not neglect the theory part which is also very important. I would recommend it to anyone who wants to learn java or prepare for a job interview or new job. Additionally, CodeGym creates a community where you can get help or meet interesting people.

30% Off With CodeGym subscription by sending the coupon: devdiaries30 to support@codegym.cc

If you are interested in full access - CodeGym has provided my blog readers with a 30% off discount coupon. All you need to do is to send the coupon: devdiaries30 to support@codegym.cc. If you have any questions, please do not hesitate to contact me!