Domain Architecture

The domain is organised around these fundamental concepts:

  • Users represent authenticated users (or the system user).

  • Messages refer to the messages sent by users and by the system.

  • Commands refer to a specific type of message, a command, which will (when authorized) trigger a change to entities in the system (e.g. /rename room My Room).

  • Rooms are the message rooms users may join, to which messages are sent.

  • Memberships represent the membership status history (PendingApproval, Joined, Revoked, etc.) of a user for a given room. This history is used to authorize users using an AuthService.

The uniquely identifiable entity representing a message in the system is a SentMessage. Before being sent, there are a few value objects which represent how messages are processed and dispatched:

  1. An IncomingMessage represents a new message received by the system. At this point it has not been processed or stored.
  2. A message prefixed with a forward slash (e.g. /help) is considered to be a command, represented by an IncomingCommand.
  3. A newly generated message which has not yet been dispatched is a DraftMessage.
  4. A draft message is sent to the message Dispatcher which will send the message to the appropriate room and store it in the room's history as a SentMessage.

The entrypoint to the messaging pipeline is the MessagesService. A simplified view of this service looks like this:

%%{init:{"theme":"dark"}}%% stateDiagram state MessagesService { direction LR state if_state <<choice>> [*] --> if_state if_state --> IncomingCommand: isCommand if_state --> IncomingMessage: !isCommand IncomingMessage --> Dispatcher Dispatcher --> SentMessage IncomingCommand --> CommandService CommandService --> Dispatcher }
%%{init:{"theme":"default"}}%% stateDiagram state MessagesService { direction LR state if_state <<choice>> [*] --> if_state if_state --> IncomingCommand: isCommand if_state --> IncomingMessage: !isCommand IncomingMessage --> Dispatcher Dispatcher --> SentMessage IncomingCommand --> CommandService CommandService --> Dispatcher }
stateDiagram
  state MessagesService {
  direction LR
    state if_state <<choice>>
    [*] --> if_state
    if_state --> IncomingCommand: isCommand
    if_state --> IncomingMessage: !isCommand
    IncomingMessage --> Dispatcher
    Dispatcher --> SentMessage
    IncomingCommand --> CommandService
    CommandService --> Dispatcher
  }

A message identified as an IncomingCommand will be parsed and (if it is a valid command) executed.

%%{init:{"theme":"dark"}}%% stateDiagram state CommandService { direction LR [*] --> IncomingCommand IncomingCommand --> TokenizedCommand : tokenize TokenizedCommand --> ParsedCommand : parse ParsedCommand --> Dispatcher : execute Dispatcher }
%%{init:{"theme":"default"}}%% stateDiagram state CommandService { direction LR [*] --> IncomingCommand IncomingCommand --> TokenizedCommand : tokenize TokenizedCommand --> ParsedCommand : parse ParsedCommand --> Dispatcher : execute Dispatcher }
stateDiagram
  state CommandService {
    direction LR
    [*] --> IncomingCommand
    IncomingCommand --> TokenizedCommand : tokenize
    TokenizedCommand --> ParsedCommand : parse
    ParsedCommand --> Dispatcher : execute
    Dispatcher
  }

The CommandService applies the following steps to match parsed commands and execute them with the appropriate use case:

  • The tokenizeCommand function perfoms a lexing role, tokenizing the incoming commmand based on whitespace and returning a TokenizedCommand which allows for easy parsing.
  • The ParseCommandUseCase class parses tokenized commands, returning a ParsedCommand, a type safe discriminated union of all possible commands.
  • Once the command is parsed, the CommandService will match the parsed command and execute the corresponding use case.
  • These use cases will update entities and send messages via the Dispatcher as appropriate.