serverlesstechnology / cqrs

A lightweight, opinionated CQRS and event sourcing framework.
Other
346 stars 36 forks source link

Aggregate ID unavailable in command handler #84

Closed paulkre closed 7 months ago

paulkre commented 7 months ago

Is there a reason why the aggregate ID is not being passed to the handle function of the aggregate trait? In a lot of situations this forces the inclusion of the ID in the aggregate‘s commands and events, so it can be stored inside the aggregate‘s struct. This is not ideal though because it adds unnecessary redundancy and verbosity, and implies that the ID is mutable (which it is not). It would be very helpful if the aggregate ID was available inside the handle function because then it would be possible to e.g. query relations of the aggregate instance via the aggregate services.

davegarred commented 7 months ago

Hi @paulkre,

You're right on that it shouldn't be included within the payload of most commands, but this is almost always present in the initial create command and should be added to the created event. If you need the aggregate id later (a fairly common occurrence) you should then store it on the aggregate using the appropriate event handler (the aggregate's apply method).

Example from cqrs-demo:

#[derive(Serialize, Deserialize)]
pub struct BankAccount {
    account_id: String,
    ...
}

#[async_trait]
impl Aggregate for BankAccount {
    ...
    fn apply(&mut self, event: Self::Event) {
        match event {
            BankAccountEvent::AccountOpened { account_id } => {
                self.account_id = account_id;
            }
            ...
        }
    }
}

You'll then have access to the aggregate id for any future events.

paulkre commented 7 months ago

Hi @davegarred, thanks for the quick reply. I know that it is possible to get the aggregate's ID by storing it in the aggregate's struct. I just think that this is not a good solution because then the ID becomes part of the aggregate instance's mutable state. The ID could be changed accidentally by some later event which will always be undesirable. Also I don't understand why it should be a best practice to include the aggregate's ID in the Created event because in the event store the event will always be associated with the correct ID anyways. There's no need to persist the ID in multiple locations.

So my question is: Why don't we just let the framework maintain the aggregate ID by passing it to the handle function?

davegarred commented 7 months ago

I just think that this is not a good solution because then the ID becomes part of the aggregate instance's mutable state. The ID could be changed accidentally by some later event which will always be undesirable.

This really depends on the context around why we want access to the aggregate id within the command handler. Let's assume it is for lookup purposes to call some outside database or service, in that case we want to be able to change what we're storing on the aggregate. Locking yourself to a particular id within your business logic gives up much of the flexibility that CQRS and event sourcing provide.

Also I don't understand why it should be a best practice to include the aggregate's ID in the Created event because in the event store the event will always be associated with the correct ID anyways.

The aggregate id does not always contain "domain information", in which case it would not need to ever be in the payload. Usually, however, it does (e.g., bank account id, customer id, etc.), in which case it should be included in the payload of an event, usually the event where the id is assigned (i.e., the created).

If you're using the aggregate id to make decisions within the aggregate, then it is "domain information" and it should be included.

paulkre commented 7 months ago

Ok now I understand. Thanks for the explanation.