Skip to content

nanlong/eventide

Repository files navigation

eventide

Crates.io Documentation License: MIT OR Apache-2.0

中文版本: README.zh.md

A pragmatic Rust toolkit for Domain-Driven Design with first-class support for event sourcing and CQRS, organised as a hexagonal workspace.

eventide-application  →  eventide-domain  ←  eventide-macros
                                 ↑
                                 └──── (optional) infra-sqlx adapters

Workspace layout

Crate What it ships
eventide Umbrella crate. Re-exports the three layers and a curated prelude.
eventide-domain Pure domain layer: aggregates, entities, value objects, events, repository traits.
eventide-application Application layer: command bus, query bus, handlers, application context.
eventide-macros Procedural macros (#[entity], #[entity_id], #[domain_event], #[value_object]).

Quick start

Most users only need to depend on the umbrella crate:

[dependencies]
eventide = "0.1"

A single dependency covers the common path. Macro-generated Serialize / Deserialize derives route through eventide's internal serde re-export, so the macros work without a direct serde dependency. eventide::tokio exposes the Tokio runtime (with the eventing feature, default), and eventide::async_trait lets you write #[async_trait] on your own trait impls. Add serde / tokio / async-trait to your own Cargo.toml only when you reach for them outside these re-exports (extra derives, custom #[serde(...)] attributes, advanced runtime features). See eventide/README.md for details.

A minimal aggregate looks like this:

use eventide::prelude::*;

#[entity_id]
struct AccountId(String);

#[entity(id = AccountId)]
#[derive(Clone)]
struct BankAccount {
    balance: i64,
}

#[derive(Debug)]
enum AccountCommand {
    Deposit(i64),
    Withdraw(i64),
}

#[domain_event(version = 1)]
enum AccountEvent {
    #[event(event_type = "account.deposited")]
    Deposited { amount: i64 },
    #[event(event_type = "account.withdrawn")]
    Withdrawn { amount: i64 },
}

impl Aggregate for BankAccount {
    const TYPE: &'static str = "bank_account";
    type Command = AccountCommand;
    type Event = AccountEvent;
    type Error = DomainError;

    fn execute(&self, cmd: AccountCommand) -> Result<Vec<AccountEvent>, DomainError> {
        match cmd {
            AccountCommand::Deposit(n) if n > 0 => Ok(vec![AccountEvent::Deposited {
                id: uuid::Uuid::new_v4().to_string(),
                aggregate_version: self.version().next().value(),
                amount: n,
            }]),
            AccountCommand::Withdraw(n) if n > 0 && self.balance >= n => {
                Ok(vec![AccountEvent::Withdrawn {
                    id: uuid::Uuid::new_v4().to_string(),
                    aggregate_version: self.version().next().value(),
                    amount: n,
                }])
            }
            _ => Err(DomainError::invalid_command("invalid amount or insufficient")),
        }
    }

    fn apply(&mut self, event: &AccountEvent) {
        match event {
            AccountEvent::Deposited { aggregate_version, amount, .. } => {
                self.balance += amount;
                self.version = Version::from_value(*aggregate_version);
            }
            AccountEvent::Withdrawn { aggregate_version, amount, .. } => {
                self.balance -= amount;
                self.version = Version::from_value(*aggregate_version);
            }
        }
    }
}

Build, test, run examples

Requires Rust 1.85+ (workspace uses Rust 2024 edition).

cargo build --workspace
cargo test  --workspace --all-features

# Domain examples
cargo run -p eventide-domain --example event_upcasting
cargo run -p eventide-domain --example event_repository
cargo run -p eventide-domain --example snapshot_repository
cargo run -p eventide-domain --example eventing_inmemory

# Application examples
cargo run -p eventide-application --example inmemory_command_bus
cargo run -p eventide-application --example inmemory_query_bus

Why eventide

  • Hexagonal-friendly. The domain layer defines abstractions (EventRepository, SnapshotRepository, AggregateRepository); infrastructure crates implement them.
  • Event sourcing built in. Aggregates emit events, the engine persists them, and an upcasting chain handles schema evolution without touching historical data.
  • CQRS by default. Separate CommandBus and QueryBus with type-safe handler registration.
  • Procedural macros that disappear. #[entity], #[entity_id], #[domain_event], and #[value_object] remove the boilerplate so business invariants stay readable.
  • Async-native. Tokio-based event engine with cooperative dispatch, retry, and dead-letter handling.
  • No vendor lock-in. The domain crate has zero database dependencies; pick (or write) the infrastructure adapter you need.

Feature flags (umbrella crate)

Flag Default What it does
eventing yes Asynchronous event subsystem (bus / engine / dispatcher / reclaimer) on top of tokio.
macros yes Re-export eventide-macros as eventide::macros.
application yes Re-export eventide-application as eventide::application.
infra-sqlx no Opt-in sqlx conversions on serialized events / snapshots for Postgres-backed event stores.

Pick individual sub-crates instead of the umbrella when you want finer control:

[dependencies]
eventide-domain = "0.1"
eventide-macros = "0.1"
# Skip eventide-application if you do not need the bus.

Documentation

Each sub-crate also ships its own README.md with focused documentation.

License

Licensed under either of

at your option.

Contributing

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

Pragmatic Rust toolkit for Domain-Driven Design with first-class event sourcing and CQRS. Hexagonal architecture, async-native, zero database dependencies in the domain layer.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Contributors