Question

I have been creating a POC for a part of our system but I'm finding some trouble communicating some of the ideas behind it. This takes ideas from Event Sourcing and Event Driven Architecture but trying to learn step by step.

I thought I had my reasons and motivations clear, but to be honest, I don't find good arguments for all the questions I'm getting. I would greatly appreciate your opinions/comments/ideas and of course, any links or material that you may have.

In one part of our system, we needed to make sure that we would have a complete audit trail for the operations that were happening. We wanted to be able to see all the transitions that brought an object to some state and re-run them to verify that the result made sense. What I did envision was the following:

  • A list of immutable events carefully crafted to express what was happening in the relevant entities. They all must have a defined set of fields that would work as a contract.

  • A set of aggregates (could be more than one) that would know how to reconstruct themselves reading partially this stream of events. This ability to build the current or past states, I understand, is basically event sourcing.

  • I also wanted to set the first stone towards a more event-driven architecture. So other parts of the application, even other systems, could maintain a pointer to the stream to read new changes that had just happened. These consumers could be interested only in part of the stream and they would rely only on the signature of the events (Interface segregation principle: no client should be forced to depend on methods it does not use) decoupling as much as possible themselves from the "origin" system.

I have been getting several questions from this:

1) Duplication

I can't translate exactly our domain, so I hope this makes partial sense. Let's assume this stream of events for an app where we can create exams and users can take them:

- Exam-question-created  (Id: Question1, Text: 'st')
- Exam-question-created  (Id: Question2, Text: 'st else' )
- Exam-initiated         (Id: User1)
- Exam-question-answered (Question: Question1, User: User1, Text: 'st', Reply: 'One reply')

As you can see, I store the text of the question answered also in the exam-question-answered and this information was already there in the exam-question-created event. I must say that in this example it doesn't make much sense, but I wanted to tie the text that the user saw when answering the question. Even that I try to put just the minimal information that defines the business event, for me, this time has a business meaning. I also want to be able to have other systems that listen just to exam-question-answered and incorporate the text to the event. I don't know, let's imagine that we have a real-time dashboard that only shows questions as they are answered, and we want to show the text.

Is this duplication fundamentally wrong? (again, the example is a bit weak, bear with me). I understand that the events are the source of truth, but adding the text to the event does not make anything incorrect. And if I have an Exam aggregate, I understand it has the responsibility to, given the event name and the payloads, reconstruct its state making whatever use it needs from the payloads. This implementation belongs to the aggregate. This one would just discard (for example) the text property of Exam-question-answered. Am I wrong?. The ability to reconstruct its state is there, and we do not have any wrong data in the store.

Would it be better to store without duplication and hydrate the data with extra info when it is going to be given to consumers?

2) The expectation to deep merge

And this question is also connected with the previous one. At some point, it appeared the expectation in the team that the aggregates could be built replaying the events and merging the payloads.

So, again an example (although not very good), if we have an 'Exam-question-removed' they expect to see a {id: X, deleted: true} in the payload. This property is already present in some parts of our system (they know it) and for some aggregations it makes sense. They expect to merge payloads for some of the events automatically in getting in return something that makes sense.

My interpretation is different. The fact that you have codified immutable business facts as events enable building different aggregations from them. If you are rebuilding an exam, for example, this payload can be convenient, but if you are building an analytics dashboard, just to put one example, maybe the Exam-question-removed means that you are going to reduce -1 from some counter. You don't need the 'deleted' part of the payload. For me, what makes sense is to code the event with the minimal data that gives meaning to the business fact. If you know all that you need to know with:

name: exam-question-removed data: { id: 1 }

There is no need for more. The process replying events must be responsible to know what to do with each event towards the final state that it wants to rebuild, it's just a function:

f(current_state, event (name, payload)) => new_state

You must implement f by yourself.

Did I get this completely wrong?. Any indication to material about this or how to properly design events would be greatly appreciated. If it supports what I say great, but really looking forward to find if this is incorrect.

Was it helpful?

Solution

A set of aggregates (could be more than one) that would know how to reconstruct themselves reading partially this stream of events. This ability to build the current or past states, I understand, is basically event sourcing.

In Event sourcing it's not just you can, you need to reconstruct the Aggregate's current state from the events. Aggregate snapshots don't count as they are just an optimisation and the system must work the same without them (indeed slower).

As you can see, I store the text of the question answered also in the exam-question-answered and this information was already there in the exam-question-created event. I must say that in this example it doesn't make much sense, but I wanted to tie the text that the user saw when answering the question. Even that I try to put just the minimal information that defines the business event, for me, this time has a business meaning. I also want to be able to have other systems that listen just to exam-question-answered and incorporate the text to the event. I don't know, let's imagine that we have a real-time dashboard that only shows questions as they are answered, and we want to show the text. Is this duplication fundamentally wrong?

In general the domain events contain only the information that can't be derived from the past events and that is needed by the Aggregate to completely reconstruct its state. There are however cases when you may add redundant information to the events, for example when the Read-models would be a lot simpler.

So, again an example (although not very good), if we have an 'Exam-question-removed' they expect to see a {id: X, deleted: true} in the payload.

This deleted:true is really redundant. The fact that the question is deleted comes from the event-type itself, exam-question-removed, there is no need for such a property. I suggest you to remove it as it makes one wonder why such a property was needed.

Is there anything wrong with the idea that the same events could build different aggregates. Example: An stream of exam-question-created and exam-question-answered can be used to build an Exam aggregate but also a MonthlyAnsweredQuestions aggregate. I know the second sounds as a projection, but let's assume it has business meaning to and it makes sense to consider it as aggregate

Yes, it would be very wrong. Aggregates should not depends on anything else but their own events. Maybe it is not an Aggregate but an event sourced Saga/Process manager (that would create/send commands based on previous events emitted by some Aggregates).

You said: "There are however cases when you may add redundant information to the events". So this is not strictly forbidden then, right?. Is it a design choice?. Should it be stored in an extra metadata field?

Yes, it's not strictly forbidden and I don't think that it must be stored in a metadata field. The event represents facts that happened, it's not wrong to capture some context in which they happened.

OTHER TIPS

In one part of our system, we needed to make sure that we would have a complete audit trail for the operations that were happening. We wanted to be able to see all the transitions that brought an object to some state and re-run them to verify that the result made sense.

There are at least two different ways of understanding the audit trail.

In one approach, we keep track of all of the input messages in the system; you rebuild the current state of the service by replaying the inputs. Each "event" describes an input from the world outside the model -- QuestionAnsweredByHuman.

In the other approach, we keep track of the output messages in the system; which is to say, each time the model changes its internal state, we write a memo describing the change. "Current state" is rebuilt by replaying the list of changes made by the model.

In the trivial case, where you get the model exactly right and never have to change it again, these two approaches are nearly indistinguishable. The excitement begins when you need to change the domain logic in your model.

But even the trivial case is made more complicated by the fact that the messages we use to recover the current state are not necessarily the same as the messages we want to publish to the world. One of the important things to recognize is that the Domain Event pattern makes sense even when we aren't using events as the representation of persisted state.

Duplication

- Exam-question-created  (Id: Question1, Text: 'st')
- Exam-question-answered (Question: Question1, User: User1, Text: 'st')

As input messages: there's nothing wrong with having redundant information in the input side. You might need to think about what it means when the text that is answered doesn't match the created version.

As output messages (persistence): it's a little bit odd to include the duplicate information, as you aren't describing a change. But it isn't wrong, by any means

As output messages (publish): putting redundant information into an output message can make sense if it helps the downstream consumers act on the message alone - it reduces the number of queries the downstream consumers need to perform to collect the "redundant" information that they lack.

Deep Merge

You must implement f by yourself.

There is a lot to unpack behind that statement.

Your basic notion that an aggregation is just a fold over an event history is correct. If you need a different aggregation, then you pass a different aggregation method to the fold.

Where it can get tricky: events/business processes/schema change over time. So you want to incorporate into your design something that makes that change as cost effective as possible.

In particular, if a change in your domain model means that I have to drop everything and implement new folds, then something has gone sideways.

There are at least two different ways of dealing with that.

One is to be extremely disciplined about your message formats. See Greg Young's Versioning in an Event Sourced System, or your favorite available book on messaging.

Another is to pay attention to service autonomy; Udi Dahan on finding service boundaries is a good starting point. The basic idea being that if your service is the authority, then it should be responsible for answering questions about the state. The published event serves primarily as a notification that previously cached copies of some queries are no longer valid, and that client services should refresh their caches by querying the authority at their earliest convenience.

how to properly design events

Events are messages; you need to invest time up front understanding how schema evolution works. Hohpe's book on Enterprise Integration Patterns is a good starting point; the user guides for various message formats (Avro, protocol buffers, Cap'n Proto) will help to steer you in the right direction.

Licensed under: CC-BY-SA with attribution
scroll top