I am new to DDD, and I have ran into a problem with unique constraints. I have a problem where one of the fields (a value object) on my aggregate root cannot be a duplicate value. For example, a user has a unique username.
My domain layer contains:
public class User {
private UUID id;
private Username username;
private User(UUID id, Username username) {
this.id = id;
this.username = username;
}
public void rename(Username username) {
if (!username.equals(username)) {
this.username = username;
EventBus.raise(new UserRenamedEvent(username));
}
}
public UUID getId() {
return id;
}
public Username getUsername() {
return username;
}
public static User create(UUID id, Username username) {
User user = new User(id, username);
EventBus.raise(new UserCreatedEvent(user));
return user;
}
}
Username:
public record Username(String name) {
// Validation on username
}
As well as a simple CRUD repository interface, implemented in the infrastructure layer.
My application layer contains:
UserSerivce:
public interface UserService {
UUID createUser(Username username);
// Get, update and delete functions...
}
And UserServiceImpl:
public class UserServiceImpl implements UserService {
public UUID createUser(Username username) {
// Repository returns an Optional<User>
if (userRepository.findByName(username).isPresent()) {
throw new DuplicateUsernameException();
}
User user = User.create(UUID.randomUUID(), username);
repsitory.save(user);
return user.getId();
}
}
This solution doesn't feel right, as preventing duplicate usernames is domain logic, and should not be in the application layer. I have also tried creating a domain service to check for duplicate usernames, but this also feels wrong as the application service has access to the repository and can do this by itself.
If the user was part of an aggregate I would do the validation at the aggregate root level, but as user is the aggregate this isn't possible. I would really like to know the best place to validate the unique constraint.
CodePudding user response:
This solution doesn't feel right, as preventing duplicate usernames is domain logic, and should not be in the application layer.
Correct.
I have also tried creating a domain service to check for duplicate usernames, but this also feels wrong as the application service has access to the repository and can do this by itself.
Yes, the application service could do the work by itself, but you have taken a conscious decision to create a dedicated layer for those 'tricky' bits where the domain aggregate cannot do the work on its own and you do not want to leak domain knowledge into the application service.
There's nothing wrong with this. Just don't let 'Domain Service' become a your default approach whenever something looks a bit tricky. You'll end up with an anaemic domain model and all the logic sitting in Domain Services. But, sometimes, a Domain Service is the only pragmatic solution and feel free to use them when all else fails.
The other alternative is to search up "Domain Events". They keep a better separation in my view but demands more effort and plumbing.
Here's a C# introduction, but just as applicable to the world of java.
CodePudding user response:
This solution doesn't feel right, as preventing duplicate usernames is domain logic, and should not be in the application layer.
There are at least two common answers.
One is to accept that "domain layer" vs "application layer" is a somewhat artificial distinction, and to not get too hung up on where the branching logic happens. We're trying to ship code that meets a business need; we don't get bonus points for style.
Another approach is to separate the act of retrieving some information from the act of deciding what to do with it.
Consider:
public UUID createUser(Username username) {
return createUser(
UUID.randomUUID(),
username,
userRepository.findByName(username).isPresent()
);
}
UUID createUser(UUID userId, Username username, boolean isPresent) {
if (isPresent) {
throw new DuplicateUsernameException();
}
User user = User.create(userId, username);
repository.save(user);
return user.getId();
}
What I'm hoping to make clear here is that we actually have two different kinds of problems to address. The first is that we'd like to separate the I/O side effects from the logic. The second is that our logic has two different outcomes, and those outcomes are mapped to different side effects.
// warning: pseudo code ahead
select User.create(userId, username, isPresent)
case User(u):
repository.save(u)
return u.getId()
case Duplicate:
throw new DuplicateUsernameException()
In effect, User::create
isn't returning User
, but rather some sort of Result
that is an abstraction of all of the different possible outcomes of the create operation. We want the semantics of a process, rather than a factory.
So we probably wouldn't use the spelling User::create
, but instead something like CreateUser::run
or CreateUser::result
.
There are lots of ways you might actually perform the implementation; you could return a discriminated union from the domain code, and then have some flavor of switch statement in the application code, or you could return an interface, with different implementations depending on the result of the domain logic, or....
It largely depends on how important it is that the domain layer is "pure", how much additional complexity you are willing to take on to get it, including how you feel about testing the designs, which idioms your development team is comfortable with, and so on.