cqrs-rust-lib 0.8.0

An opinionated implementation of CQRS/Event Sourcing with pluggable storage backends (InMemory, PostgreSQL, MongoDB, SurrealDB)
Documentation
# Migration Guide: Aggregate / CommandHandler Split

## Overview

This guide describes the migration from the legacy single `Aggregate` trait to the new split design with two separate traits:

- **`Aggregate`**: Handles event processing and state management
- **`CommandHandler`**: Handles command processing to produce events

This separation follows the Single Responsibility Principle and enables more flexible architectures (e.g., aggregates that can be reconstituted from events without command handling capabilities).

## Summary of Changes

### Before (Legacy)

```rust
#[async_trait::async_trait]
impl Aggregate for MyAggregate {
    const TYPE: &'static str = "my_aggregate";

    type CreateCommand = CreateCmd;
    type UpdateCommand = UpdateCmd;
    type Event = MyEvent;
    type Services = MyServices;
    type Error = MyError;

    fn aggregate_id(&self) -> String { ... }
    fn with_aggregate_id(self, id: String) -> Self { ... }

    async fn handle_create(...) -> Result<Vec<Self::Event>, Self::Error> { ... }
    async fn handle_update(...) -> Result<Vec<Self::Event>, Self::Error> { ... }

    fn apply(&mut self, event: Self::Event) -> Result<(), Self::Error> { ... }
    fn error(status: StatusCode, details: &str) -> Self::Error { ... }
}
```

### After (New)

```rust
#[async_trait::async_trait]
impl Aggregate for MyAggregate {
    const TYPE: &'static str = "my_aggregate";

    type Event = MyEvent;
    type Error = MyError;

    fn aggregate_id(&self) -> String { ... }
    fn with_aggregate_id(self, id: String) -> Self { ... }

    fn apply(&mut self, event: Self::Event) -> Result<(), Self::Error> { ... }
    fn error(status: StatusCode, details: &str) -> Self::Error { ... }
}

#[async_trait::async_trait]
impl CommandHandler for MyAggregate {
    type CreateCommand = CreateCmd;
    type UpdateCommand = UpdateCmd;
    type Services = MyServices;

    async fn handle_create(...) -> Result<Vec<Self::Event>, Self::Error> { ... }
    async fn handle_update(...) -> Result<Vec<Self::Event>, Self::Error> { ... }
}
```

## Step-by-Step Migration

### Step 1: Update Imports

Add `CommandHandler` to your imports from `cqrs_rust_lib`.

**Before:**
```rust
use cqrs_rust_lib::{Aggregate, CqrsContext, ...};
```

**After:**
```rust
use cqrs_rust_lib::{Aggregate, CommandHandler, CqrsContext, ...};
```

### Step 2: Split the Aggregate Implementation

#### 2.1 Keep in `impl Aggregate`:
- `const TYPE`
- `type Event`
- `type Error`
- `fn aggregate_id(&self)`
- `fn with_aggregate_id(self, id: String)`
- `fn apply(&mut self, event: Self::Event)`
- `fn error(status: StatusCode, details: &str)`

#### 2.2 Move to `impl CommandHandler`:
- `type CreateCommand`
- `type UpdateCommand`
- `type Services`
- `async fn handle_create(...)`
- `async fn handle_update(...)`

### Step 3: Add the CommandHandler Implementation Block

Create a new `impl CommandHandler for YourAggregate` block with the `#[async_trait::async_trait]` attribute.

### Step 4: Verify Compilation

```bash
cargo build
```

## Complete Example

### Before Migration

```rust
use cqrs_rust_lib::{Aggregate, CqrsContext};
use http::StatusCode;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Account {
    pub id: String,
    pub owner: String,
    pub balance: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CreateCommand {
    Open { owner: String },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UpdateCommand {
    Deposit { amount: f64 },
    Withdraw { amount: f64 },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AccountEvent {
    Opened { owner: String },
    Deposited { amount: f64 },
    Withdrawn { amount: f64 },
}

impl cqrs_rust_lib::Event for AccountEvent {
    fn event_type(&self) -> String {
        match self {
            Self::Opened { .. } => "Opened".into(),
            Self::Deposited { .. } => "Deposited".into(),
            Self::Withdrawn { .. } => "Withdrawn".into(),
        }
    }
}

#[async_trait::async_trait]
impl Aggregate for Account {
    const TYPE: &'static str = "account";

    type CreateCommand = CreateCommand;
    type UpdateCommand = UpdateCommand;
    type Event = AccountEvent;
    type Services = ();
    type Error = std::io::Error;

    fn aggregate_id(&self) -> String {
        self.id.clone()
    }

    fn with_aggregate_id(mut self, id: String) -> Self {
        self.id = id;
        self
    }

    async fn handle_create(
        &self,
        command: Self::CreateCommand,
        _services: &Self::Services,
        _context: &CqrsContext,
    ) -> Result<Vec<Self::Event>, Self::Error> {
        match command {
            CreateCommand::Open { owner } => Ok(vec![AccountEvent::Opened { owner }]),
        }
    }

    async fn handle_update(
        &self,
        command: Self::UpdateCommand,
        _services: &Self::Services,
        _context: &CqrsContext,
    ) -> Result<Vec<Self::Event>, Self::Error> {
        match command {
            UpdateCommand::Deposit { amount } => Ok(vec![AccountEvent::Deposited { amount }]),
            UpdateCommand::Withdraw { amount } => Ok(vec![AccountEvent::Withdrawn { amount }]),
        }
    }

    fn apply(&mut self, event: Self::Event) -> Result<(), Self::Error> {
        match event {
            AccountEvent::Opened { owner } => self.owner = owner,
            AccountEvent::Deposited { amount } => self.balance += amount,
            AccountEvent::Withdrawn { amount } => self.balance -= amount,
        }
        Ok(())
    }

    fn error(_status: StatusCode, details: &str) -> Self::Error {
        std::io::Error::other(details.to_string())
    }
}
```

### After Migration

```rust
use cqrs_rust_lib::{Aggregate, CommandHandler, CqrsContext};
use http::StatusCode;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Account {
    pub id: String,
    pub owner: String,
    pub balance: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CreateCommand {
    Open { owner: String },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UpdateCommand {
    Deposit { amount: f64 },
    Withdraw { amount: f64 },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AccountEvent {
    Opened { owner: String },
    Deposited { amount: f64 },
    Withdrawn { amount: f64 },
}

impl cqrs_rust_lib::Event for AccountEvent {
    fn event_type(&self) -> String {
        match self {
            Self::Opened { .. } => "Opened".into(),
            Self::Deposited { .. } => "Deposited".into(),
            Self::Withdrawn { .. } => "Withdrawn".into(),
        }
    }
}

// ============================================================
// AGGREGATE TRAIT: Event processing and state management
// ============================================================
#[async_trait::async_trait]
impl Aggregate for Account {
    const TYPE: &'static str = "account";

    type Event = AccountEvent;
    type Error = std::io::Error;

    fn aggregate_id(&self) -> String {
        self.id.clone()
    }

    fn with_aggregate_id(mut self, id: String) -> Self {
        self.id = id;
        self
    }

    fn apply(&mut self, event: Self::Event) -> Result<(), Self::Error> {
        match event {
            AccountEvent::Opened { owner } => self.owner = owner,
            AccountEvent::Deposited { amount } => self.balance += amount,
            AccountEvent::Withdrawn { amount } => self.balance -= amount,
        }
        Ok(())
    }

    fn error(_status: StatusCode, details: &str) -> Self::Error {
        std::io::Error::other(details.to_string())
    }
}

// ============================================================
// COMMAND HANDLER TRAIT: Command processing
// ============================================================
#[async_trait::async_trait]
impl CommandHandler for Account {
    type CreateCommand = CreateCommand;
    type UpdateCommand = UpdateCommand;
    type Services = ();

    async fn handle_create(
        &self,
        command: Self::CreateCommand,
        _services: &Self::Services,
        _context: &CqrsContext,
    ) -> Result<Vec<Self::Event>, Self::Error> {
        match command {
            CreateCommand::Open { owner } => Ok(vec![AccountEvent::Opened { owner }]),
        }
    }

    async fn handle_update(
        &self,
        command: Self::UpdateCommand,
        _services: &Self::Services,
        _context: &CqrsContext,
    ) -> Result<Vec<Self::Event>, Self::Error> {
        match command {
            UpdateCommand::Deposit { amount } => Ok(vec![AccountEvent::Deposited { amount }]),
            UpdateCommand::Withdraw { amount } => Ok(vec![AccountEvent::Withdrawn { amount }]),
        }
    }
}
```

## Checklist for Migration

Use this checklist to ensure complete migration:

- [ ] Update import statement to include `CommandHandler`
- [ ] Remove `type CreateCommand` from `impl Aggregate`
- [ ] Remove `type UpdateCommand` from `impl Aggregate`
- [ ] Remove `type Services` from `impl Aggregate`
- [ ] Remove `async fn handle_create` from `impl Aggregate`
- [ ] Remove `async fn handle_update` from `impl Aggregate`
- [ ] Create new `#[async_trait::async_trait] impl CommandHandler for YourAggregate`
- [ ] Add `type CreateCommand` to `impl CommandHandler`
- [ ] Add `type UpdateCommand` to `impl CommandHandler`
- [ ] Add `type Services` to `impl CommandHandler`
- [ ] Add `async fn handle_create` to `impl CommandHandler`
- [ ] Add `async fn handle_update` to `impl CommandHandler`
- [ ] Run `cargo build` to verify compilation
- [ ] Run tests to verify behavior

## AI Agent Instructions

For AI agents performing this migration automatically:

### Detection

Search for files containing the old pattern:
```
grep -r "impl Aggregate for" --include="*.rs" | grep -v "impl CommandHandler"
```

Identify aggregates that have `type CreateCommand` inside `impl Aggregate` blocks.

### Transformation Algorithm

1. **Parse** the `impl Aggregate` block
2. **Extract** the following items:
   - `type CreateCommand`
   - `type UpdateCommand`
   - `type Services`
   - `async fn handle_create`
   - `async fn handle_update`
3. **Remove** extracted items from `impl Aggregate`
4. **Create** new `impl CommandHandler` block with extracted items
5. **Update** imports to include `CommandHandler`
6. **Preserve** all other code unchanged

### Validation

After transformation:
1. Run `cargo build` - must succeed
2. Run `cargo test` - all tests must pass
3. Verify no duplicate implementations exist

### Edge Cases

- **Multiple aggregates in one file**: Process each separately
- **Custom derives or attributes**: Preserve all attributes
- **Documentation comments**: Keep doc comments with their associated items
- **Feature flags on associated types**: Preserve `#[cfg(...)]` attributes

## Troubleshooting

### Error: "the trait bound `YourAggregate: CommandHandler` is not satisfied"

You forgot to implement `CommandHandler` for your aggregate. Add the `impl CommandHandler` block.

### Error: "duplicate definitions for `CreateCommand`"

You still have `type CreateCommand` in both `impl Aggregate` and `impl CommandHandler`. Remove it from `impl Aggregate`.

### Error: "method `handle_create` is not a member of trait `Aggregate`"

The method was not moved to `impl CommandHandler`. Move `handle_create` and `handle_update` to the `CommandHandler` implementation.

## Benefits of This Change

1. **Separation of Concerns**: Event application logic is separate from command handling
2. **Flexibility**: Aggregates can be reconstituted from events without command handling
3. **Testability**: Each trait can be tested independently
4. **Read Model Support**: Enables aggregate state reconstruction for read models without command processing overhead