Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
475 views
in Technique[技术] by (71.8m points)

domain driven design - How to model chat messages in an event-sourced system?

Context: I'm exploring to build an event sourced system / PoC using EventStoreDB (separate event stream per aggregate) with Node.JS/TypeScript. One part of the system is a 1:1 customer support chat. When a chat message is created, a push notification is sent to the user, including an update to the app's badge number (total unread message count). I'm wondering what's the best way to model the aggregates / bounded contexts.

Question 1: where to put the chat messages?

Question 2: how to handle a customer's unread message badge counter?

Since chat messages are by themselves already timed events, they seem like they could easily fit in an event sourced system. Still, I'm looking for advise on how to best model the aggregates:

Option A: Since each chat message has its own lifecycle (they can be edited, have a read status that gets updated, etc.), ChatMessage could be an aggregate on its own. This would explode the number of aggregates (and thus streams), but that might not really be such an issue for EventStoreDB. However, to send the notification for a message, we'll need to know the total number of unread messages (so info on other aggregates). But how should the push notification sending "saga" / "process manager" (which is the correct term?) know what badge counter to send with the notification? Should it keep its own state / read model with the current counter for each customer based on all the event it has seen?

Option B: Another way might be to have a list of messages under the Customer aggregate root. That way, Customer could have a counter for the number of unread messages and a fold of all the events would give me that number. However, here I'm afraid the large number of chat message events for the Customer aggregate root gets in the way of "simple" Customer behavior. E.g. when processing a Customer command, we'd first get the current state by folding all events (assume no snapshotting is used), which means applying all those chat events, even to just do something with the current name of the customer.

Option C: Or should these be in different bounded contexts? So have the Customer with it's contact details in a bounded context, and have a separate bounded context for chat (or communications in general), where both have a Customer aggregate root sharing only the UUID of the customer? Would that be best of both worlds, or would that give other challenges?

Is any of the options the way to go? Or is there another, better option? Or am I just missing the point entirely ;) (don't wanna rule that out)

Any advice is much appreciated!

question from:https://stackoverflow.com/questions/66055683/how-to-model-chat-messages-in-an-event-sourced-system

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Event Sourcing describes a way to (re)create state, by storing every change as an event. This does not include how those events get persisted or snapshotted, or how they are read and distributed.

I always start from the User Interface. Because that's where you should know which information you want to display and which actions can be executed.

For example there could be the following Commands (or actions executed by the User Interface):

  • SendMessage(receiverId, content)

  • MarkMessageAsRead(messageId)

Your server will then check if the provided data is valid and create the related Events:

class SupportChatMessageAggregate {
  MessageId messageId;
  UserId senderId;
  UserId receiverId;
  String content;
  boolean readByReceiver;

  // depending on framework and personal preference, this could
  // also be a method: handle(SendMessage command, CurrentUser currentUser)
  constructor(SendMessage command, CurrentUser currentUser) {
    validate(command); // throws Exception if invalid
    // for example if content is empty,
    // or if currentUser is not allowed to send messages to receiverId

    publishEvent(new MessageSentEvent(
      command.getMessageId(),
      currentUser.getUserId(),
      command.getReceiverId(),
      command.getContent()
    ));
  }

  handle(MarkMessageAsRead command, CurrentUser currentUser) {
    validate(command); // throws Exception if invalid
    // for example check if currentUser == receiver

    publishEvent(new MessageMarkedAsReadEvent(
      command.getMessageId(),
      currentUser.getUserId()
    ));
  }

  ...

}

Now when you want to know the badge counter for a User, you simply add up all the MessageSentEvents where receiver = currentUser, and subtract all the MessageMarkedAsReadEvents of the currentUser.

This could be done for example within the UnreadSupportChatMessageCountAggregate, that is responsible for providing the current unreadMessages value based on the MessageSentEvents and MessageMarkedAsReadEvents for a given User. A pretty boring Aggregate, but it does the job.

That's Event Sourcing: You simply have a bunch of events, and if you want to query some data, you just fetch all related events, process them, and get your result. If you use separate event streams per aggregate or just have a single stream for all events is an implementation detail (or depends on the event store you use).

Depending on the number of events this can be extremely fast, or very slow. That's where snapshots and/or read models (from CQRS) come in handy. But for plain Event Sourcing this is not required.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...