Crates.io | eventcore |
lib.rs | eventcore |
version | 0.1.8 |
created_at | 2025-07-07 01:34:07.953474+00 |
updated_at | 2025-07-23 17:32:11.860934+00 |
description | Multi-stream aggregateless event sourcing library with type-driven development |
homepage | |
repository | https://github.com/jwilger/eventcore |
max_upload_size | |
id | 1740562 |
size | 1,310,425 |
A type-safe event sourcing library implementing multi-stream event sourcing with dynamic consistency boundaries - commands that can atomically read from and write to multiple event streams.
Traditional event sourcing forces you into rigid aggregate boundaries. EventCore breaks free with:
# Cargo.toml
[dependencies]
eventcore = "0.1"
eventcore-postgres = "0.1" # or your preferred adapter
use eventcore::{prelude::*, require, emit};
use eventcore_macros::Command;
use eventcore_postgres::PostgresEventStore;
#[derive(Command)]
struct TransferMoney {
#[stream]
from_account: StreamId,
#[stream]
to_account: StreamId,
amount: Money,
}
#[async_trait]
impl CommandLogic for TransferMoney {
type State = AccountBalances;
type Event = BankingEvent;
// type StreamSet is auto-generated by #[derive(Command)] ✅
fn apply(&self, state: &mut Self::State, event: &StoredEvent<Self::Event>) {
match &event.payload {
BankingEvent::MoneyTransferred { from, to, amount } => {
state.debit(from, *amount);
state.credit(to, *amount);
}
}
}
async fn handle(
&self,
read_streams: ReadStreams<Self::StreamSet>,
state: Self::State,
input: Self::Input,
_: &mut StreamResolver,
) -> CommandResult<Vec<StreamWrite<Self::StreamSet, Self::Event>>> {
require!(state.balance(&input.from_account) >= input.amount, "Insufficient funds");
let mut events = vec![];
emit!(events, &read_streams, input.from_account, BankingEvent::MoneyTransferred {
from: input.from_account.to_string(),
to: input.to_account.to_string(),
amount: input.amount,
});
Ok(events)
}
}
let store = PostgresEventStore::new(config).await?;
let executor = CommandExecutor::new(store);
let command = TransferMoney {
from_account: StreamId::try_new("account-alice")?,
to_account: StreamId::try_new("account-bob")?,
amount: Money::from_cents(10000)?,
};
let result = executor.execute(&command, command).await?;
The #[derive(Command)]
macro automatically generates boilerplate from #[stream]
fields:
#[derive(Command)]
struct TransferMoney {
#[stream]
from_account: StreamId,
#[stream]
to_account: StreamId,
amount: Money,
}
// Automatically generates:
// - TransferMoneyStreamSet phantom type for compile-time stream safety
// - Helper method __derive_read_streams() for stream extraction
// - Enables type Input = Self pattern for simple commands
Discover additional streams during execution:
async fn handle(...) -> CommandResult<Vec<StreamWrite<...>>> {
require!(state.is_valid(), "Invalid state");
if state.requires_approval() {
stream_resolver.add_streams(vec![approval_stream()]);
}
let mut events = vec![];
emit!(events, &read_streams, input.account, AccountEvent::Updated { ... });
Ok(events)
}
Optimistic locking prevents conflicts automatically. Just execute your commands - version checking and retries are handled transparently.
eventcore/ # Core library - traits and types
eventcore-postgres/ # PostgreSQL adapter
eventcore-memory/ # In-memory adapter for testing
eventcore-examples/ # Complete examples
See eventcore-examples/ for complete working examples:
# Setup
nix develop # Enter dev environment
docker-compose up -d # Start PostgreSQL
# Test
cargo nextest run # Fast parallel tests
cargo test # Standard test runner
# Bench
cargo bench # Performance benchmarks
Based on current testing with PostgreSQL backend:
Note: Performance optimized for correctness and multi-stream atomicity. See Performance Report for detailed benchmarks and system specifications.
EventCore follows strict type-driven development. See CLAUDE.md for our development philosophy.
Licensed under the MIT License. See LICENSE for details.