5

I am new to the concept of DDD (with CQRS) so please, bare with me for this moment.

I understand what are Aggregates, Entities, Domain Events and Commands but I really have a terrible time comprehending the relationships between them.

My questions are: Does a Command, well, command an Aggregate to do something? Or does it create entities? Or both and I have full control (and responsibility) over how I use this tool? Does a Command emit Domain Events? If so, why and when?

I believe my question is solely based on my lack of experience in this field. Maybe a quick example would help?

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.

How would you approach this in regards of my previous question?

1 Answer 1

4

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.

  1. You could start with an InviteUserToEventCommand, having a idOfUserToBeInvited attribute.
  2. 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.
  3. 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?).
  4. 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);
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.