Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling events in an cross-aggregate relationships, and aggregate state

I have recently started my first ever effort in developing a ticketing web application using domain driven design principles combined with event sourcing and CQRS.

Since it is my first attempt away from the traditional CRUD approach, and into the DDD world, I am sure I have many things designed wrongly as DDD needs a lot of effort to come up with the right separation of domains, bounded contexts etc.

In my design, I have command handlers that accept a command, initiate a Job (a unit of work) they load the aggregates that are needed from the aggregate repository (which loads aggregates from event store by replaying events), and they manipulate the aggregates through each aggregate's exposed actions and then close the Job.

The aggregates expose actions which actually issue events. For instance, company.Create(firmName, address, taxid, ...) issues a CompanyCreated event, and applies it unto itself. When the Job is about to complete, all events from all aggregates loaded within the context of that Job are gathered and persisted by the event store.

Now, I came in a situation, which I am sure is very common, where I have relations between aggregates. For instance a Customer has Contacts, or a SupportAgent is member of a Department. Those are aggregates in my design.

Lets take the Department example. A Department's state consists of title, a description, some other properties, and a list of SupportAgent ids of those agents that are members of that department. A SupportAgent's state consists of a name, surname, phone number, email, ... and a list of Department ids of those departments this agent is member of.

Now when a Command of type AddAgentToDepartment(agentId, departmentId) is handled, two events are issued. A DepartmentAdded is issued for the corresponding agent that will add the department id into the agent's state, and a SupportAgentAdded is issued for the corresponding department that will add the agent id into the department's state.

My first question is: Is it right to keep the ids of related aggregates into the state of an aggregate? By 'right' I mean is it a best practice? Or is there another way (eg keeping relationships in a kind of 'DepartmentMemberManager' entity/aggregate or something. Actually this entity or whatever is kind of a singleton here. Is there such thing in DDD world)?

My other thought is about event replay. In the previous example, two events are issued, but in order to update the views, only one of them needs to be handled because both events describe the exact same transition in the system's state (an agent and a department are linked). I choose to handle only SupportAgentAdded event to update the views. My event handler executes an SQL script to update the corresponding database Tables to reflect the current state of the system.

What happens in case we need to replay some events to bring only a certain aggregate's view in a consistent state? Specifically, when I want to replay events for a support agent, only DepartmentAdded events will be replayed, and those events are not handled by anyone, so the views won't be updated. Is it right to replay partially some events or all events in the event store should be replayed in order to bring the whole system into a consistent state?

If you are a DDD and ES expert or at least you have experience, I would like to get some hints on what you may see I am doing, or thinking, wrong and what direction should I look at.

like image 337
Thanasis Ioannidis Avatar asked Jan 27 '23 06:01

Thanasis Ioannidis


2 Answers

CQRS means Command-Query Responsibility Segregation. There are two sides C - command, Write side. Q - Query, Read side.

Aggregates are living on the C - command side, and can only execute a command. Aggregates cannot be queried. So in your example, your Agent's command handler simply cannot talk to some Department aggregate

Read model can be queried though, so nothing prevents you from querying some Departments read model. But there is a consistency problem.

Aggregate instance is consistent according to its event stream, meaning nothing can change the state of this aggregate while you are performing a command. So your aggregate is a transaction boundary - everything in its state is consistent and everything outside its state - probably inconsistent.

So if you are dealing with anything outside aggregate's state - you are dealing with potentially inconsistent data - in your example your department might already be deleted, but read model is not yet showing this.

Now, aggregate is not an entity. The very name "aggregate" implies that there are several "things" there. Aggregate is an object that can execute commands and ensure business rules. Which means that Command is sent to one aggregate.

Choosing your aggregates is a main domain design activity in CQRS/ES system. Mistakes are very expensive, because you'll need to deal with event versioning and refactoring (Greg Young recently wrote a book about it)

So, in your example we do have a single command:

AddAgentToDepartment(agentId, departmentId)

The first question - which aggregate is it addressed to? Remember - one command for one aggregate. This is a design decision, that depends on your system. I would think of things like: can Agent still be Agent without this command? I guess so, tomorrow you'll have no departments, but, say, Products, and Agent should not be affected. Can Department be a department without this command? Unlikely - it is a thing to group agents. So I would make a Department an aggregate that receives

AddAgentToDepartment(departmentId, params: { agentIdToAdd })

And department aggregate will care of business rules (can't add same agent twice, cannot remove non-existing agent, etc.)

Remember, you can easily have a read model for Agent, that lists all departments for a given agent, you just don't need a departments in Agent aggregate's state, because you won't have department-related commands sent to agent.

In case when all Agent-related commands should be aware of departments, you might made Agent a target of AddAgentToDepartment. And Department aggregate would have minimum set of commands: create, rename, delete.

My first question is: Is it right to keep the ids of related aggregates into the state of an aggregate?

No. Command is sent to a single aggregate, and command handler can deal only with aggregate's state that is computed from this aggregate's event stream. Keeping id of other aggregates won't help, because you cannot use them anywhere.

My other thought is about event replay. In the previous example, two events are issued, but in order to update the views, only one of them needs to be handled because both events describe the exact same transition in the system's state (an agent and a department are linked).

Your event stream should make sence to a domain expert. In your example, a single AgentAddedToDepartment event makes sense. Two events - doesn't. In majority of cases single command should generate a single event.

What happens in case we need to replay some events to bring only a certain aggregate's view in a consistent state? Specifically, when I want to replay events for a support agent, only DepartmentAdded events will be replayed, and those events are not handled by anyone, so the views won't be updated. Is it right to replay partially some events or all events in the event store should be replayed in order to bring the whole system into a consistent state?

It looks like you mixed write and read side. Replaying events on one side should not affect another side in any way. Our reSolve framework works this way:

On the 'C' - command (Write) side, upon receiving a command, aggregate's state is restored from this aggregate's event stream, by querying event store: give me all events for aggregate 12345.

On the 'Q" - query (Read) side, there are no aggregates, there are read models. Those read models are usually built from multiple type of events for different aggregates. When you need to rebuild a read model - you are querying event store: give me all events that matches my criteria. Then you are applying those events to read model (it can take some time), and when read-model is up-to-date, it can subscribe to current event stream and update itself in real time.

like image 191
Roman Eremin Avatar answered Feb 08 '23 23:02

Roman Eremin


In my design, I have command handlers that accept a command, initiate a Job (a unit of work) they load the aggregates that are needed from the aggregate repository (which loads aggregates from event store by replaying events), and they manipulate the aggregates through each aggregate's exposed actions and then close the Job.

You are likely to get some push back on that. Modifying multiple aggregates within a single transaction (unit of work) gets really complicated when the aggregates are stored in different places. If everything is in "one database", you can get away with it. But as soon as you introduce a second database, you in effect introduce a "distributed transaction", which is a lot more awkward to deal with.

In many modern discussions, the underlying assumption is that each aggregate is a "transaction boundary", meaning you only modify a single aggregate in any given transaction. That in turn means a much more forgiving consistency constraint -- and that a single "command messages" that is supposed to impact multiple aggregates in the model may end up performing a partial update.

What happens in case we need to replay some events to bring only a certain aggregate's view in a consistent state?

The usual answer is that the views are managed independently of the aggregates. There's no guarantee that there will be one view per aggregate (some aggregates may not have a view of their own, others might have more than one).

The way that it usually works is that we can use a correlation identifier (for instance, the identifier of the aggregate) to filter a stream of events. So a given read model doesn't need to replay all events, just the subset(s) of events.

Is it right to replay partially some events or all events in the event store should be replayed in order to bring the whole system into a consistent state?

Horses for courses - partial replay is often used to update read models.

You may find it useful to review this 2014 talk by Greg Young

like image 44
VoiceOfUnreason Avatar answered Feb 08 '23 23:02

VoiceOfUnreason