eventastic_postgres 0.5.0

A postgres event store for eventastic
Documentation
# Eventastic

A type-safe event sourcing and CQRS library for Rust with PostgreSQL persistence.

## Features

- **Strongly-typed aggregates and events** - Define your domain model with Rust structs and enums
- **Mandatory transactions** for ACID guarantees
- **Built-in idempotency** prevents duplicate event processing
- **Optimistic concurrency control** detects conflicting modifications
- **Transactional outbox pattern** for reliable side effects
- **Snapshot optimization** for fast aggregate loading
- **In-memory repository** for testing and development

## Quick Start

Define your domain aggregate and events:

```rust
use eventastic::aggregate::{Aggregate, Context, Root, SideEffect};
use eventastic::event::DomainEvent;
use eventastic::memory::InMemoryRepository;
use eventastic::repository::Repository;

#[derive(Clone, Debug)]
struct BankAccount {
    id: String,
    balance: i64,
}

#[derive(Clone, Debug, PartialEq, Eq)]
enum AccountEvent {
    Opened { event_id: String, account_id: String, initial_balance: i64 },
    Deposited { event_id: String, amount: i64 },
    Withdrawn { event_id: String, amount: i64 },
}

impl DomainEvent for AccountEvent {
    type EventId = String;
    fn id(&self) -> &Self::EventId {
        match self {
            AccountEvent::Opened { event_id, .. } => event_id,
            AccountEvent::Deposited { event_id, .. } => event_id,
            AccountEvent::Withdrawn { event_id, .. } => event_id,
        }
    }
}

// Define a no-op side effect type
#[derive(Clone, Debug, PartialEq, Eq)]
struct NoSideEffect;

impl SideEffect for NoSideEffect {
    type SideEffectId = String;
    fn id(&self) -> &Self::SideEffectId {
        unreachable!("No side effects are produced")
    }
}

impl Aggregate for BankAccount {
    const SNAPSHOT_VERSION: u64 = 1;
    type AggregateId = String;
    type DomainEvent = AccountEvent;
    type ApplyError = String;
    type SideEffect = NoSideEffect;

    fn aggregate_id(&self) -> &Self::AggregateId {
        &self.id
    }

    fn apply_new(event: &Self::DomainEvent) -> Result<Self, Self::ApplyError> {
        match event {
            AccountEvent::Opened { account_id, initial_balance, .. } => {
                Ok(BankAccount {
                    id: account_id.clone(),
                    balance: *initial_balance,
                })
            }
            _ => Err("Account must be opened first".to_string()),
        }
    }

    fn apply(&mut self, event: &Self::DomainEvent) -> Result<(), Self::ApplyError> {
        match event {
            AccountEvent::Opened { .. } => Err("Account already exists".to_string()),
            AccountEvent::Deposited { amount, .. } => {
                self.balance += amount;
                Ok(())
            }
            AccountEvent::Withdrawn { amount, .. } => {
                if self.balance >= *amount {
                    self.balance -= amount;
                    Ok(())
                } else {
                    Err("Insufficient funds".to_string())
                }
            }
        }
    }

    fn side_effects(&self, _event: &Self::DomainEvent) -> Option<Vec<Self::SideEffect>> {
        None
    }
}
```

Use the aggregate with transactions:

```rust
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let repository = InMemoryRepository::<BankAccount>::new();

    // Create new account using the Root trait
    let mut account: Context<BankAccount> = BankAccount::record_new(
        AccountEvent::Opened {
            event_id: "evt-1".to_string(),
            account_id: "acc-123".to_string(),
            initial_balance: 1000,
        }
    )?;

    // Deposit money
    account.record_that(AccountEvent::Deposited {
        event_id: "evt-2".to_string(),
        amount: 500,
    })?;

    // Save with transaction
    let mut transaction = repository.begin_transaction().await?;
    transaction.store(&mut account).await?;
    transaction.commit()?;

    // Load account
    let loaded_account = repository.load(&"acc-123".to_string()).await?;
    assert_eq!(loaded_account.state().balance, 1500);

    Ok(())
}
```

## Architecture

Eventastic is built around four core concepts:

- **Aggregates** - Domain entities that apply events to update their state
- **Events** - Immutable records of what happened in your domain
- **Context** - Wrapper that tracks aggregate state and uncommitted events
- **Repository** - Persistence layer with transactional guarantees

## Why Eventastic?

### Transaction-First Design

Unlike many event sourcing libraries, Eventastic requires transactions for all write operations. This provides:

- **ACID compliance** - All changes are atomic and consistent
- **Idempotency** - Duplicate events are detected and handled gracefully
- **Concurrency safety** - Optimistic locking prevents data races
- **Side effect reliability** - External operations are processed via outbox pattern

### Rust Benefits

Using Rust provides compile-time guarantees:

- Events must implement required traits (DomainEvent, Clone, etc.)
- Aggregates must handle all event types in match statements
- Error handling is explicit with Result types
- No null pointer exceptions or runtime type errors

### Production Ready

Eventastic includes features needed for production systems:

- Automatic snapshot creation and loading
- Comprehensive error types with structured information
- Transaction-based consistency guarantees

## Persistence

The library provides multiple repository implementations:

- `eventastic::memory::InMemoryRepository` - For testing and development
- `eventastic_postgres::PostgresRepository` - For production PostgreSQL storage with:
  - Event and snapshot storage with versioning
  - Full transaction support with optimistic concurrency control
  - Optional encryption for sensitive data
  - Database migrations support
- `eventastic_outbox_postgres::TableOutbox` - Transactional outbox pattern for reliable side effect processing

## Examples

See the `examples/` directory for complete implementations:

- **Bank** - Full banking domain demonstrating:
  - Account creation and management
  - Transaction processing
  - Side effects via outbox pattern
  - Idempotency and concurrency handling