6

I have been working with a software strongly relying on the event sourcing and CQRS patterns for a few time and I'm trying to figure out how to properly model the domain events in relation to the needs of the read models.

Let's try to describe the question I'm asking myself in a more formal way. In an event sourcing application a domain event is an object which represents something that happened in the domain and that is relevant to the application. By definition a domain event should be self contained in the sense that it should contain all the relevant information about what happened in the domain, so that it is immediately usable without asking information to other sources.

Usually, when event sourcing is used along with CQRS, the events are published by using some sort of event-bus (it could be as simple as an in-memory event bus or as complex as a full fledged service bus) so that all the interested denormalizers are able to subscribe to them and update some sort of read model accordingly. In all the examples that I have seen the denormalizer code simply reads the properties contained in the event and operates some sort of CRUD over the read model (usually a relational or a documental database): the only source of truth is the event and no other source of truth is queried in order to get the information needed to update the read model. This makes sense to me, doing this way all the read models are independent and can be rebuilded freely because they don't depend on each others (reading read model A while handling an event in order to update the read model B is considered an anti-pattern).

Provided that all the previous statements are correct (please, let me know if they aren't), here is my doubt: how can I properly design the taxonomy of a domain event (I mean the properties) if I don't know in advance who are all the possible read models of my system (and, in general, we don't due to changing requirements for instance) ?

Just to better clarify the point, imagine an e-commerce domain where users are able to purchase books. One of the most important domain events is the following:

public class BookPurchased
{
  public Guid UserId { get; set; }
  public Guid BookId { get; set; }
  public Datetime PurchaseDate { get; set; }
  public Guid PurchaseId { get; set; }
}

Imagine that at some point in time, the need to support mobile clients arises. In this scenario we need an highly denormalized read model for the client, so that with a single call to a rest api it is able to get all the data it needs to render a detail page about the purchase (ideally we would like a fast api, which can read the data with a single query to a single read model, without the need to aggregate data retrieved with multiple queries). In this case, the previously designed event is not well suited for a denormalizer in charge of filling the new read model, because information like the full name of the user or the book title, for instance, are missing. A better shape for the event, in this new scenario, could be the following:

public class BookPurchased
{
  public Guid UserId { get; set; }
  public string UserFirstName { get; set; }
  public string UserLastName { get; set; }
  public Guid BookId { get; set; }
  public string BookTitle { get; set; }
  public Datetime PurchaseDate { get; set; }
  public Guid PurchaseId { get; set; }
}

What is the correct way to go in this kind of scenario ?

Should we model "fat" events containing all the possible information (even if they are not strictly needed for the known application use cases) in advance, so that we can support highly denormalized read models in case we need ?

Putting it in another way, is it fine to include some properties in a domain event class only to serve the need of a single specific read model ?

Or maybe should we opt for small and focused read models (so that we can have thinner domain events and the rebuild process is faster) and use some sort of "smart aggregator" (like GraphQL) when we need to create highly denormalized views of the current state of the system ?

9
  • Is there a reason that you cannot retrieve FirstName and LastName during the process of denormalization from UserId? That would seem to be the most appropriate place to aggregate the extra data. Commented Oct 17, 2018 at 15:46
  • @king-side-slide as you correctly pointed out, the right place where getting the information is the event handler for BookPurchased event, but the question is: what is the source of that information ? You are handling an event, so the obvious source is the event itself. The problem is that the event is raised by an aggregate and it is possibly that the aggregate doesn't own the information you need. So, where can you find the missing pieces ? Commented Oct 18, 2018 at 7:44
  • @king-side-slide The naive approach could be querying some other read model, but this is wrong for several reasons. This way you create a dependency between different read models and this is evil because you are not free to change them independently: they shouldn't know about each other because they are just two different ways to represent the current state of the system. Moreover, the read models are eventually consistent and you should not base a business rule of any kind about their content: at any point in time you are not sure that a read model contain an updated information Commented Oct 18, 2018 at 7:46
  • @king-side-slide There is one last side effect involved in querying another read model: you should be free to rebuild the read models independently whenever you need it and you can't do it if they depend on each other. The good news is that (as pointed out in the answer) is that you can subscribe to different events from your denormalizer and maintain a private state that you can use to get the missing information that you need if you want to denormalize the current state of the system each time you receive an event of type BookPurchased Commented Oct 18, 2018 at 7:52
  • You are misunderstanding. How is it that your read model has the concept of UserId at all? At some point in time there must have been a UserCreated-type event, right? Is FirstName and LastName not part of that event? If so, the read model already has this information and could therefore use it as part of it's denormalization strategy. Does that make sense? Commented Oct 18, 2018 at 14:51

1 Answer 1

4

The domain events should contain the data that is required and owned by the Write model/the Aggregate. If the Readmodel may need additional data that is not strictly required by the Aggregate and the Aggregate owns it, then you may add this information to the domain event if this would really make your Readmodel much simpler. For example, you may include the old/previous value of some property.

In your case, the additional information needed by the Readmodel is not owned by the Aggregate (so it seems any way). This means that you should not include it. You would have to pass it in the command as well, all the way down to the domain event. The Aggregate would need to forward it by including in the domain event and this means that the Aggregate is forced to know about other Aggregates, maybe other bounded contexts.

You may however subscribe to the domain events emitted by other Aggregates. In your case, you may subscribe to the UserCreated event emitted by the User Aggregate and you could maintain a private/local state of user names. When the BookPurchased event comes in, you may fetch the UserFirstName and UserLastName from the private state.

Lately, in order to limit the proliferation and duplication of private states in Readmodels, I started to use Queries. The Queries are answered by some canonical readmodels. They may also be pushed, when the answer gets updated. Although I use may own framework for this, for more information, you may take a look at the Axon framework, section Queries.

5
  • I'm not sure to fully understand what you wrote in the second paragraph of your answer. The point highlited in the first paragraph is that an aggregate, when generating an event, should only include the information that it owns. Do you consider acceptable the idea of including additional information in the command and use the aggregate as a "middle man" which only forwards those information inside the event (along with other information that it owns) ? Is this acceptable or you consider this a code smell ? Commented Oct 17, 2018 at 9:30
  • @EnricoMassone I don't find it acceptable, it is not a good design solution, it's a design smell. Commented Oct 17, 2018 at 9:32
  • thanks for claryfing. So, the reason you consider this a code smell is that, as you wrote, doing so would "leak" knowledge about other aggregates (maybe other contexts) inside the aggregate which raises the event. To summarize, the pattern you advice is maintaining a private state inside the denormalizer. That private state should be updated by subscribing to all the useful domain events. By using this private state a denormalizer is able to do its business even with an highly denormalized read model. Correct ? Commented Oct 17, 2018 at 9:36
  • @EnricoMassone yes, exactly. You can however simplify this proliferation of private states by using Queries, like I wrote in the answer. Commented Oct 17, 2018 at 9:40
  • thanks I'll further explore towards this direction. What is emerging is that the stuff I call denormalizers, which are in other terms a specialized type of event handler, should be thought as complex pieces of software with their own state and their own business logic, not simple stateless CRUD executors Commented Oct 17, 2018 at 9:44

Your Answer

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

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