Aggregateless, Multi-Stream Event-Sourcing for Rust
  • Rust 98.9%
  • PLpgSQL 0.7%
  • Nix 0.4%
Find a file
dependabot[bot] 009e8c8591
deps(deps): bump nutype from 0.6.2 to 0.7.0 (#381)
Bumps [nutype](https://github.com/greyblake/nutype) from 0.6.2 to 0.7.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/greyblake/nutype/blob/master/CHANGELOG.md">nutype's
changelog</a>.</em></p>
<blockquote>
<h3>v0.7.0 - 2026-04-25</h3>
<ul>
<li><strong>[BREAKING]</strong> Rename <code>derive_unsafe</code> to
<code>derive_unchecked</code> (both the feature flag and the
attribute).</li>
<li><strong>[FEATURE]</strong> Support <code>cfg_attr</code> for
conditional derives, e.g. <code>cfg_attr(feature = &quot;serde&quot;,
derive(Serialize, Deserialize))</code>. Supports complex predicates and
multiple entries.</li>
<li><strong>[FEATURE]</strong> Support <code>where</code> clauses in
generic newtypes, including Higher-Ranked Trait Bounds (HRTB) like
<code>for&lt;'a&gt; &amp;'a C: IntoIterator</code> (see <a
href="https://redirect.github.com/greyblake/nutype/issues/160">#160</a>).</li>
<li><strong>[FEATURE]</strong> Ability to control constructor visibility
with <code>constructor(visibility = ...)</code> attribute (see <a
href="https://redirect.github.com/greyblake/nutype/issues/211">#211</a>).</li>
<li><strong>[FEATURE]</strong> Add <code>len_utf16_min</code> and
<code>len_utf16_max</code> validators for string types to validate
UTF-16 code unit length (useful for JavaScript interop) (see <a
href="https://redirect.github.com/greyblake/nutype/issues/162">#162</a>).</li>
<li><strong>[FEATURE]</strong> Ability to derive <a
href="https://docs.rs/valuable/0.1.1/valuable/trait.Valuable.html"><code>Valuable</code></a>
(requires <code>valuable</code> feature).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e951afd4b9"><code>e951afd</code></a>
Merge pull request <a
href="https://redirect.github.com/greyblake/nutype/issues/238">#238</a>
from greyblake/preapre-v070</li>
<li><a
href="a3ea381419"><code>a3ea381</code></a>
Prepare v0.7.0</li>
<li><a
href="7464ec78ae"><code>7464ec7</code></a>
Merge pull request <a
href="https://redirect.github.com/greyblake/nutype/issues/235">#235</a>
from greyblake/cfg-attr-v2</li>
<li><a
href="f33c0979d7"><code>f33c097</code></a>
Address clippy: use core:#️⃣:Hash</li>
<li><a
href="0c915cdb24"><code>0c915cd</code></a>
Refactor: introduce HasGeneratedParseError trait</li>
<li><a
href="20d55498f7"><code>20d5549</code></a>
Refactor: DRY, extract process_conditional_derives()</li>
<li><a
href="1541bc3f4d"><code>1541bc3</code></a>
Import validate_all_derive_traits</li>
<li><a
href="93b7298b0b"><code>93b7298</code></a>
Fix wording</li>
<li><a
href="51437fe7c3"><code>51437fe</code></a>
Use impls to test trait bounds</li>
<li><a
href="e4b1e9c9e5"><code>e4b1e9c</code></a>
Move validation logic out of models.rs</li>
<li>Additional commits viewable in <a
href="https://github.com/greyblake/nutype/compare/v0.6.2...v0.7.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 13:21:37 -07:00
.cargo feat: add load-testing/stress-testing suite (#370) 2026-04-15 12:41:12 -07:00
.claude chore: remove worktree references from project configuration (#356) 2026-04-12 15:42:28 +00:00
.github chore(ci): remove benchmark workflow (#365) 2026-04-13 02:04:02 +00:00
blueprints feat(sqlite): make encryption opt-in, re-export rusqlite, add BYOC constructors (#379) 2026-04-27 17:08:19 +00:00
docs refactor: expose projection config via free function API, then reduce public surface (#357) 2026-04-12 10:24:37 -07:00
eventcore deps(deps): bump nutype from 0.6.2 to 0.7.0 (#381) 2026-04-27 13:21:37 -07:00
eventcore-bench deps(deps): bump nutype from 0.6.2 to 0.7.0 (#381) 2026-04-27 13:21:37 -07:00
eventcore-examples deps(deps): bump nutype from 0.6.2 to 0.7.0 (#381) 2026-04-27 13:21:37 -07:00
eventcore-macros deps(deps): bump the minor-and-patch group across 1 directory with 11 updates (#383) 2026-04-27 12:21:00 -07:00
eventcore-memory deps(deps): bump the minor-and-patch group across 1 directory with 11 updates (#383) 2026-04-27 12:21:00 -07:00
eventcore-postgres deps(deps): bump nutype from 0.6.2 to 0.7.0 (#381) 2026-04-27 13:21:37 -07:00
eventcore-sqlite deps(deps): bump the minor-and-patch group across 1 directory with 11 updates (#383) 2026-04-27 12:21:00 -07:00
eventcore-stress deps(deps): bump nutype from 0.6.2 to 0.7.0 (#381) 2026-04-27 13:21:37 -07:00
eventcore-testing deps(deps): bump nutype from 0.6.2 to 0.7.0 (#381) 2026-04-27 13:21:37 -07:00
eventcore-types deps(deps): bump nutype from 0.6.2 to 0.7.0 (#381) 2026-04-27 13:21:37 -07:00
scripts chore: configure claude code (#134) 2025-10-13 00:39:13 +00:00
.env.example refactor(eventcore-postgres): replace testcontainers with docker-compose (#224) 2025-12-27 21:39:26 +00:00
.gitattributes chore: update SDLC setup to v1.2.1 (#296) 2026-02-17 08:18:07 -08:00
.gitignore chore: remove worktree references from project configuration (#356) 2026-04-12 15:42:28 +00:00
.pre-commit-config.yaml feat(postgres): add PostgreSQL event store implementation (#169) 2025-12-03 19:04:03 -08:00
AGENTS.md docs: comprehensive documentation overhaul (#350) 2026-04-11 17:22:26 -07:00
Cargo.lock deps(deps): bump nutype from 0.6.2 to 0.7.0 (#381) 2026-04-27 13:21:37 -07:00
Cargo.toml chore: release v0.8.0 (#382) 2026-04-27 17:33:19 +00:00
CLAUDE.md docs: comprehensive documentation overhaul (#350) 2026-04-11 17:22:26 -07:00
CODE_OF_CONDUCT.md I-001: Single-Stream Command Execution (End-to-End) (#137) 2025-10-22 16:02:56 -07:00
COMPLIANCE_CHECKLIST.md I-001: Single-Stream Command Execution (End-to-End) (#137) 2025-10-22 16:02:56 -07:00
CONTRIBUTING.md docs: comprehensive documentation overhaul (#350) 2026-04-11 17:22:26 -07:00
docker-compose.yml refactor(eventcore-postgres): replace testcontainers with docker-compose (#224) 2025-12-27 21:39:26 +00:00
event_core.png Create documentation website with mdBook 2025-07-04 23:21:59 -07:00
flake.lock Cleanup (#131) 2025-10-12 09:04:50 -07:00
flake.nix chore: adopt han plugins, blueprints, and project conventions (#330) 2026-04-09 18:12:17 +00:00
LICENSE Create LICENSE 2025-06-28 14:34:57 -07:00
README.md docs: comprehensive documentation overhaul (#350) 2026-04-11 17:22:26 -07:00
release-plz.toml feat: enforce lockstep major/minor versioning in release automation (#219) 2025-12-27 14:43:57 +00:00
REVIEW.md chore: add refactor-sequencing guardrail and review check (#354) 2026-04-12 15:21:21 +00:00
rust-toolchain.toml Cleanup (#131) 2025-10-12 09:04:50 -07:00
SECURITY.md docs: comprehensive documentation overhaul (#350) 2026-04-11 17:22:26 -07:00
skills-lock.json chore: migrate from marvin-sdlc to agent-skills bootstrap (#303) 2026-02-22 04:52:30 +00:00

EventCore

CI License: MIT

⚠️ EXPERIMENTAL - NOT READY FOR USE

This project is in early development. APIs are unstable and subject to breaking changes. The library is not yet published to crates.io, and referenced packages/examples may be incomplete or non-existent.

Do not use this in production or depend on it for any real projects.

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.

Why EventCore?

Traditional event sourcing forces you into rigid aggregate boundaries. EventCore breaks free with:

  • Multi-stream commands: Read and write multiple streams atomically
  • Type-safe by design: Illegal states are unrepresentable
  • Dynamic stream discovery: Commands can discover streams at runtime
  • Zero boilerplate: No aggregate classes, just commands and events

Quick Start

Note: The following is a design vision, not current reality. Packages are not yet published.

# Cargo.toml (EXAMPLE - not yet available on crates.io)
[dependencies]
eventcore = { version = "0.6", features = ["memory"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
use eventcore::{
    Command, CommandError, CommandLogic, Event, NewEvents, RetryPolicy,
    StreamId, execute,
};
use eventcore_memory::InMemoryEventStore;
use serde::{Deserialize, Serialize};

// Define your events
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
enum BankAccountEvent {
    MoneyDeposited { account_id: StreamId, amount: u64 },
}

impl Event for BankAccountEvent {
    fn stream_id(&self) -> &StreamId {
        match self {
            Self::MoneyDeposited { account_id, .. } => account_id,
        }
    }
    fn event_type_name() -> &'static str { "BankAccountEvent" }
}

// Define your command with #[derive(Command)]
#[derive(Command)]
struct DepositMoney {
    #[stream]
    account_id: StreamId,
    amount: u64,
}

impl CommandLogic for DepositMoney {
    type Event = BankAccountEvent;
    type State = ();

    // apply takes OWNED state, returns OWNED state (pure fold)
    fn apply(&self, state: Self::State, _event: &Self::Event) -> Self::State {
        state
    }

    // handle is SYNC, returns Result<NewEvents<...>, CommandError>
    fn handle(&self, _state: Self::State) -> Result<NewEvents<Self::Event>, CommandError> {
        Ok(vec![BankAccountEvent::MoneyDeposited {
            account_id: self.account_id.clone(),
            amount: self.amount,
        }].into())
    }
}

// execute() is a free function — no CommandExecutor needed
let store = InMemoryEventStore::new();
let command = DepositMoney {
    account_id: StreamId::try_new("account-alice")?,
    amount: 10000,
};
execute(&store, command, RetryPolicy::new()).await?;

Key Features

Type-Safe Stream Access

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

Dynamic Stream Discovery

Some commands only learn about additional streams after inspecting state (e.g., an order references a payment-method stream). Implement StreamResolver<State> and return Some(self) from CommandLogic::stream_resolver() to opt in:

impl CommandLogic for ProcessPayment {
    type State = CheckoutState;
    type Event = CheckoutEvent;

    fn stream_resolver(&self) -> Option<&dyn StreamResolver<Self::State>> {
        Some(self)
    }

    // apply + handle omitted
}

impl StreamResolver<CheckoutState> for ProcessPayment {
    fn discover_related_streams(&self, state: &CheckoutState) -> Vec<StreamId> {
        state.payment_method_stream.clone().into_iter().collect()
    }
}

The executor deduplicates IDs returned by discover_related_streams, reads each stream exactly once, and includes every visited stream in the same optimistic concurrency check as the statically declared streams.

Built-in Concurrency Control

Optimistic locking prevents conflicts automatically. Just execute your commands - version checking and retries are handled transparently.

Architecture

eventcore/              # Core library - re-exports types, macros, and optional adapters
eventcore-types/        # Shared vocabulary - traits and types (StreamId, Event, EventStore)
eventcore-macros/       # Derive macros (re-exported by eventcore)
eventcore-postgres/     # PostgreSQL adapter (enabled via feature flag)
eventcore-sqlite/       # SQLite adapter with optional SQLCipher encryption
eventcore-memory/       # In-memory store for tests and development
eventcore-testing/      # Contract tests, EventCollector, TestScenario
eventcore-examples/     # Integration test examples

Feature Flags

Feature Default Description
macros Yes Re-exports #[derive(Command)] from eventcore-macros
postgres No Re-exports PostgresEventStore from eventcore-postgres
sqlite No Re-exports SqliteEventStore from eventcore-sqlite
# Default (includes macros)
eventcore = "0.6"

# With PostgreSQL adapter
eventcore = { version = "0.6", features = ["postgres"] }

# Without macros (rare - for minimal builds)
eventcore = { version = "0.6", default-features = false }

Examples

See eventcore-examples/ for complete working examples:

  • Banking: Account transfers with balance tracking
  • E-commerce: Order workflow with inventory management
  • Sagas: Order fulfillment with distributed transaction coordination
  • Web Framework Integration: REST API with Axum (task management system)

Documentation

Development

# Setup
nix develop              # Enter dev environment
docker-compose up -d     # Start PostgreSQL

# Test
cargo nextest run --workspace  # Fast parallel tests
cargo test --workspace         # Fallback test runner

Contributing

EventCore follows strict type-driven development. See CLAUDE.md for our development philosophy.

License

Licensed under the MIT License. See LICENSE for details.