Commands are public entry-points to your application. They provide an easy way of calling your domain, which could otherwise be complicated.
Does a Command, well, command an Aggregate to do something?
Yes.
Or does it create entities?
Also yes.
Or both and I have full control (and responsibility) over how I use this tool?
How you design a command is entirely up to you. E.g. a RegisterNewUserCommand
is likely to create a new User
entity record. But a ChangeUserPasswordCommand
on the other hand needs an existing User
entity record so that it can change its email, on it.
Does a Command emit Domain Events?
No. Command, while being a somewhat domain concept, does not emit domain events. Domain events are emitted from entities (aggregates, be it). It's up to your infrastructure setup how this is done. E.g. through a static DomainEvent.emit
call, which is kinda difficult to test, or perhaps by having your your entities implement/extend a EventEmitter
, which in turn internally tracks a Collection<Event> emittedEvents
and in your infrastructure you iterate those (before/after database transaction, depends on your needs) and react to those.
Let's consider a scenario: John wants to invite Zoe to go to the cinema with him. He sends a text message to her. She gets it and she accepts the invitation.
- You could start with an
InviteUserToEventCommand
, having a idOfUserToBeInvited
attribute.
- In command handler, you load a user for the currently authenticated user in your system (e.g. authenticated using a session), and you create a
UserEventInvitation
, having e.g. invitedByUserId
and invitedUserId
, and perhaps some other attributes which you decide to track.
- Based on the complexity of your system, you can simply call a
TextMessageService.sendMessage
method after creation of the invitation, or you could have the invitation creation emit an UserEventInvitationCreatedEvent
, listen to it and call the TextMessageService.sendMessage
in an event listener (perhaps even asynchronously?).
- Zoe decides to accept the invitation, for which you would have another command,
AcceptUserEventInvitation
, having an userEventInvitationId
, attribute.
In simple terms, the code could look something like this:
interface TextMessageService {
void sendMessage(UserId toUser, String message);
}
interface SessionService {
User getCurrentUser();
}
interface UserService {
User getUserById(UserId id);
}
interface UserEventInvitationRepository {
void save(UserEventInvitation userEventInvitation);
}
interface UserEventInvitationService {
UserEventInvitation getUserEventInvitationById(UserEventInvitationId id);
}
class UserId {
private String value;
private UserId(String value) {
this.value = value;
}
public static UserId fromString(String value) {
return new UserId(value);
}
}
class User {
private UserId id;
public UserId getId() {
return id;
}
}
class UserEventInvitationId {
private String value;
private UserEventInvitationId(String value) {
this.value = value;
}
public static UserEventInvitationId generateNew() {
return new UserEventInvitationId(UUID.randomUUID().toString());
}
public static UserEventInvitationId fromString(String value) {
return new UserEventInvitationId(value);
}
}
enum UserEventInvitationState {
PENDING,
ACCEPTED,
}
class UserEventInvitation {
private UserEventInvitationId id;
private UserId invitedByUserId;
private UserId invitedUserId;
private UserEventInvitationState state;
public UserEventInvitation(User invitedByUser, User invitedUser) {
UserId invitedByUserId = invitedUser.getId();
UserId invitedUserId = invitedUser.getId();
if (invitedByUserId.equals(invitedUserId)) {
throw new RuntimeException("Cannot invite self.");
}
this.id = UserEventInvitationId.generateNew();
this.invitedByUserId = invitedByUserId;
this.invitedUserId = invitedUserId;
this.state = UserEventInvitationState.PENDING;
}
public void accept(User acceptedBy) {
if (!UserEventInvitationState.PENDING.equals(state)) {
throw new RuntimeException("Only pending invitations might be accepted.");
}
if (!invitedUserId.equals(acceptedBy.getId())) {
throw new RuntimeException("Could not accept invitation, it's for a different user.");
}
state = UserEventInvitationState.ACCEPTED;
}
}
class InviteUserToEventCommand {
private String idOfUserToBeInvited;
public String getIdOfUserToBeInvited() {
return idOfUserToBeInvited;
}
}
class InviteUserToEventCommandHandler {
private final UserService userService;
private final SessionService sessionService;
private final UserEventInvitationRepository userEventInvitationRepository;
private final TextMessageService textMessageService;
public InviteUserToEventCommandHandler(
UserService userService,
SessionService sessionService,
UserEventInvitationRepository userEventInvitationRepository,
TextMessageService textMessageService
) {
this.userService = userService;
this.sessionService = sessionService;
this.userEventInvitationRepository = userEventInvitationRepository;
this.textMessageService = textMessageService;
}
public void handle(InviteUserToEventCommand command) {
User currentUser = sessionService.getCurrentUser();
User userToBeInvited = userService.getUserById(UserId.fromString(command.getIdOfUserToBeInvited()));
UserEventInvitation userEventInvitation = new UserEventInvitation(currentUser, userToBeInvited);
userEventInvitationRepository.save(userEventInvitation);
textMessageService.sendMessage(userToBeInvited.getId(), "You have been invited to event.");
}
}
class AcceptUserEventInvitationCommand {
private String userEventInvitationId;
public String getUserEventInvitationId() {
return userEventInvitationId;
}
}
class AcceptUserEventInvitationCommandHandler {
private final UserEventInvitationService userEventInvitationService;
private final UserEventInvitationRepository userEventInvitationRepository;
private final SessionService sessionService;
AcceptUserEventInvitationCommandHandler(
UserEventInvitationService userEventInvitationService,
UserEventInvitationRepository userEventInvitationRepository,
SessionService sessionService
) {
this.userEventInvitationService = userEventInvitationService;
this.userEventInvitationRepository = userEventInvitationRepository;
this.sessionService = sessionService;
}
public void handle(AcceptUserEventInvitationCommand command) {
UserEventInvitation toBeAccepted = userEventInvitationService.getUserEventInvitationById(
UserEventInvitationId.fromString(command.getUserEventInvitationId())
);
toBeAccepted.accept(sessionService.getCurrentUser());
userEventInvitationRepository.save(toBeAccepted);
}
}