Domain Events

Next we will need to create some domain events. Note that we qualify events with 'domain' to differentiate them from other events that might exist within our application. These events directly speak to changes in aggregate state.

In this cqrs-es framework the domain events are expected to be an enum with payloads, this will give us a single root event for each aggregate. By convention, each payload has the same name as the associate element, elements that do not require additional information use an empty payload.

The enum as well as the payloads should derive several traits.

  • Debug - used for error handling and testing.
  • Clone - the event may be passed to a number of downstream queries in an asynchronous manner, so we will want to ensure that each one gets its' own clone.
  • Serialize, Deserialize - serialization is essential for both storage and transmission to external queries.
  • PartialEq - we will be adding a lot of tests to verify that our business logic is correct.

Adding events and payloads

Let's add three self-descriptive events as part of a single enum.

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
enum BankAccountEvent {
    CustomerDepositedMoney(CustomerDepositedMoney),
    CustomerWithdrewCash(CustomerWithdrewCash),
    CustomerWroteCheck(CustomerWroteCheck)
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct CustomerDepositedMoney {
    amount: f64,
    balance: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct CustomerWithdrewCash {
    amount: f64,
    balance: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct CustomerWroteCheck {
    check_number: String,
    amount: f64,
    balance: f64,
}

Again, all of our events are in the past tense. This is important.

Our events now need to implement cqrs_es::DomainEvent<BankAccount>. The purpose of this traits single function is to update the state of an aggregate instance based on events that have happened.

The usual way to handle this is to use the enum implementation and pass the event down to the payload level applying the data. Each payload then updates the aggregate with the appropriate logic.

impl DomainEvent<BankAccount> for BankAccountEvent {
    fn apply(self, account: &mut BankAccount) {
        match self {
            BankAccountEvent::CustomerDepositedMoney(e) => {e.apply(account)},
            BankAccountEvent::CustomerWithdrewCash(e) => {e.apply(account)},
            BankAccountEvent::CustomerWroteCheck(e) => {e.apply(account)},
        }
    }
}

impl DomainEvent<BankAccount> for CustomerDepositedMoney {
    fn apply(self, account: &mut BankAccount) {
        account.balance = self.balance;
    }
}
impl DomainEvent<BankAccount> for CustomerWithdrewCash {
    fn apply(self, account: &mut BankAccount) {
        account.balance = self.balance;
    }
}
impl DomainEvent<BankAccount> for CustomerWroteCheck {
    fn apply(self, account: &mut BankAccount) {
        account.balance = self.balance;
    }
}

Note that the apply function has no return value. The act of applying an event is simply bookkeeping, the action has already taken place.

An event is a historical fact, it can be ignored, but it should never cause an error.