Learn from Our Experts

Last updated: 20/10/2023

The anemic domain model does not incorporate business logic into entities. All the business logic lives in Domain Services while entities are just data bags. Example of anemic and rich domain implementation In order to compare how Anemic and Rich Domain Model differs and what the advantages are, we will implement the same problem using both approaches. Problem statement: Implement basic user login functionality. A User can try to log in into the system. If the user enters valid credentials then login is successful. If the user enters the wrong credentials then the account is locked after 3 failed attempts. A locked account can be unlocked by support staff. Requirements: 1. User credentials are `userName` and `password` 2. If a user enters the wrong password 3 times in a row then the account is locked 3. If a user enters the correct password then the counter of failed login attempts is reset 4. Locked account can be unlocked by support staff NOTE: for simplicity reasons, the password is stored as plain text. In real-world applications, we would store the hash of the password, but to keep things simple in this example we'll work with the plain password. Anemic domain model implementation In the Anemic Domain Model entities do not have any business logic and usually expose public getters and setters. Business logic is placed in Domain Services instead. First, let's create `UserAccount` entity which stores user account attributes in the database. ```java public class UserAccount { private String password; private boolean isLocked; private int failedLoginAttempts; private String userName; public String getPassword() { return password; } public boolean getIsLocked() { return isLocked; } public int getFailedLoginAttempts() { return failedLoginAttempts; } public String getUserName() { return userName; } public void setPassword(String password) { this.password = password; } public void setIsLocked(boolean isLocked) { this.isLicked = isLocked; } public void setFailedLoginAttempts(boolean failedLoginAttempts) { this.failedLoginAttempts = failedLoginAttempts; } public void setUserName(String userName) { this.userName = userName; } } ``` Now let's create a domain service for business logic. ```java enum LoginResult { SUCCESS, INVALIDCREDENTIALS, ACCOUNTLOCKED } class AuthenticationService { // constructor and private fields code omitted for simplicity public LoginResult loginUser(String userName, String password) { UserAccount account = accountRepository.getByUserName(userName); if (account == null) { return LoginResult.INVALIDCREDENTIALS; } if (account.getIsLocked()) { return LoginResult.ACCOUNTLOCKED; } if (account.getPassword().equals(password)) { account.setFailedLoginAttempts(0); return LoginResult.SUCCESS; } int newFailedLoginAttempts = account.getFailedLoginAttempts() + 1; account.setFailedLoginAttempts(newFailedAttepts); if (newFailedLoginAttempts >= 3) { account.setIsLocked(true); return LoginResult.ACCOUNTLOCKED; } return LoginResult.INVALIDCREDENTIALS; } public void unlockAccount(String userName){ UserAccount account = accountRepository.getByUserName(userName); if (account == null) { throw new DomainException("User with given userName does not exist"); } account.setIsLocked(false); account.setFailedLoginAttempts(0); } } ``` As can be seen in the previous example, all business logic is placed in Domain Service and Entity is just a DTO that transfers data from DB to application. Now let's try to enrich the model from the previous example. We'll remove public setters and add `update` method with some logic in it. ```java public class UserAccount { private String password; private boolean isLocked; private int failedLoginAttempts; private String userName; public String getPassword() { return password; } public boolean getIsLocked() { return isLocked; } public int getFailedLoginAttempts() { return failedLoginAttempts; } public String getUserName() { return userName; } public void update(String password, boolean isLocked, int failedLoginAttempts, String userName) { if(password == null || password == "") { throw new IllegalArgumentException("password must be a non empty string"); } if(failedLoginAttempts < 0) { throw new IllegalArgumentException("failedLoginAttempts must be a greater or equal to 0"); } if(userName == null || userName == ""){ throw new IllegalArgumentException("userName must be a non empty string"); } this.password = password; this.isLocked = isLocked; this.failedLoginAttempts = failedLoginAttempts; this.userName = userName; } } ``` With these changes `AuthenticationService` becomes ```java class AuthenticationService { // constructor and private fields code omitted for simplicity public LoginResult loginUser(String userName, String password) { UserAccount account = accountRepository.getByUserName(userName); if (account == null) { return LoginResult.INVALIDCREDENTIALS; } if (account.getIsLocked()) { return LoginResult.ACCOUNTLOCKED; } if (account.getPassword().equals(password)) { account.update(account.getPassword(), account.getIsLocked(), 0, account.getUserName()); return LoginResult.SUCCESS; } int newFailedLoginAttempts = account.getFailedLoginAttempts() + 1; account.update(account.getPassword(), account.getIsLocked(), newFailedAttepts, account.getUserName()); if (newFailedLoginAttempts >= 3) { account.update(account.getPassword(), true, newFailedAttepts, account.getUserName()); return LoginResult.ACCOUNTLOCKED; } return LoginResult.INVALIDCREDENTIALS; } public void unlockAccount(String userName){ UserAccount account = accountRepository.getByUserName(userName); if (account == null) { throw new DomainException("User with given userName does not exist"); } account.update(account.getPassword(), false, 0, account.getUserName()); } } ``` Is this version better than the first one? Not really, it is the same anemic domain model with most of the business logic outside the entity. Even though we don't have public setters, `update` method acts as one big public setter. Another problem with this approach is that the `update` method is not part of our `ubiquitous language` - a user can try to log in, the account can be locked or unlocked, and so on, but `update` is not part of this language and the entity does not explicitly tell what are the operations which can be done with it. Rich domain model implementation In Rich Domain Model, an entity does not expose public setters or update methods. Instead, an entity exposes methods that change the entity state from one valid state to another. More than that - public methods are named using words from `ubiquitous language`. It allows developers and Domain experts to speak the same language. Also, new team members can learn ubiquitous language by reading source code. Let's take a look at how `UserAccount` entity can look in Rich Domain Model. ```java public class UserAccount { private String password; private boolean isLocked; private int failedLoginAttempts; private String userName; public boolean getIsLocked() { return isLocked; } public int getFailedLoginAttempts() { return failedLoginAttempts; } public String getUserName() { return userName; } public boolean tryLogin(String password) { if(this.isLocked){ throw new IllegalStateException("Account is locked"); } if( this.password.equals(password)) { this.failedLoginAttempts = 0; return true; } this.failedLoginAttempts++; if(this.failedLoginAttempts >= 3) { this.isLocked = true; } return false; } public void unlockAccount(){ this.isLocked = false; this.failedLoginAttempts = 0; } } ``` Now `UserAccount` entity clearly defines what are operations (use cases) which change the state of it. We use terms from `ubiquitous language` like `tryLogin` and `unlockAccount` for method names which makes it easier to understand what are the valid use cases when an entity changes. Think about these statements: A user can try to log in into his UserAccount. An attempt to login might be successful or not After 3 failed login attempts account is locked A locked account can be unlocked In Rich Domain Model we hardcode valid uses cases that change our entity. Also, it makes developers use the same language as domain experts - compare `tryLogin` with `setIsLocked` or `update`. We also removed the public getter `getPassword` from `AccountEntity` because we don't want to show the password on UI. Now let's take a look at how `AuthenticationService` changes. ```java class AuthenticationService { // constructor and private fields code omitted for simplicity public LoginResult loginUser(String userName, String password) { UserAccount account = accountRepository.getByUserName(userName); if (account == null) { return LoginResult.INVALIDCREDENTIALS; } if (account.getIsLocked()) { return LoginResult.ACCOUNTLOCKED; } boolean loginSuccess = account.tryLogin(password); if(loginSuccess) { return LoginResult.SUCCESS; } if (account.getIsLocked()) { return LoginResult.ACCOUNTLOCKED; } return LoginResult.INVALIDCREDENTIALS; } public void unlockAccount(String userName){ UserAccount account = accountRepository.getByUserName(userName); if (account == null) { throw new DomainException("User with given userName does not exist"); } account.unlockAccount(); } } ``` Summary DDD implemented with Rich Domain Model encapsulates business logic into entities and exposes `ubiquitous language` as public interface which allows model state manipulation. It helps developers and domain experts to ask more questions and better understand Domain. Also, it makes all team members (developers and domain experts) to use same language - `ubiquitous language` References & literature 1. Domain-Driven Design: Tackling Complexity in the Heart of Software, by Eric Evans, book 2. Implementing Domain-Driven Design, by Vaughn Vernon, book 3. Effective Aggregate Design by Vaughn Vernon 1. [Part I: Modeling a Single Aggregate](https://www.dddcommunity.org/wp-content/uploads/files/pdfarticles/Vernon20111.pdf) 2. [Part II: Making Aggregates Work Together](https://www.dddcommunity.org/wp-content/uploads/files/pdfarticles/Vernon20112.pdf) 3. [Part III: Gaining Insight Through Discovery](https://www.dddcommunity.org/wp-content/uploads/files/pdfarticles/Vernon20113.pdf)

Read more
What we offerAbout UsTechnologiesWe're hiring!BlogContact
Timisoara, str. Cugir nr. 10
office@ozius.solutions
(+40)754931941
© Ozius Solutions S.R.L. 2023. All rights reserved.
Legal Entity