es-entity
An Event Sourcing Entity Framework for Rust that simplifies building event-sourced applications with PostgreSQL.
The framework enables writing Entities that are:
- Event Sourced - Entities are hydrated via event projection.
- Idempotent - Built-in guards against duplicate operations
- Testable - Clean separation between domain logic and persistence
Persisted to postgres with:
- Minimal boilerplate - Derive macros generate repository methods automatically
- Compile-time verified - All SQL queries are checked at compile time via sqlx
- Optimistic concurrency - Automatic detection of concurrent updates via event sequences
- Pagination - Cursor-based pagination out of the box
Book | API Docs | GitHub repository | Cargo package
Free of any unsafe code#![forbid(unsafe_code)] to ensure everything is implemented in 100% safe Rust.
Quick Example
Entity
First you need your entity:
// Define your entity ID (can be any type fulfilling the traits).
entity_id!
// Define your events
// Define your entity
// derive_builder::Builder is optional but useful for hydrating
// TryFromEvents hydrates the user entity from persisted events.
Persistence
Setup your database - each entity needs 2 tables.
-- Index table for queries
(
id UUID PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
name VARCHAR UNIQUE -- Add columns you want to query by
);
-- Event storage table
(
id UUID NOT NULL REFERENCES users(id),
sequence INT NOT NULL,
event_type VARCHAR NOT NULL,
event JSONB NOT NULL,
context JSONB DEFAULT NULL,
recorded_at TIMESTAMPTZ NOT NULL,
UNIQUE(id, sequence)
);
Repository methods are generated:
// Define your repository - all CRUD operations are generated!
// // Generated Repository fns:
// impl Users {
// // Create operations
// async fn create(&self, new: NewUser) -> Result<User, EsRepoError>;
// async fn create_all(&self, new: Vec<NewUser>) -> Result<Vec<User>, EsRepoError>;
//
// // Query operations
// async fn find_by_id(&self, id: UserId) -> Result<User, EsRepoError>;
// async fn find_by_name(&self, name: &str) -> Result<User, EsRepoError>;
//
// // Update operations
// async fn update(&self, entity: &mut User) -> Result<(), EsRepoError>;
//
// // Paginated listing
// async fn list_by_id(&self, args: PaginatedQueryArgs, direction: ListDirection) -> PaginatedQueryRet;
//
// // etc
// }
Usage
async
Getting Started
Installation
Add to your Cargo.toml:
[]
= "0.9"
= "0.8.3" # Needs to be in scope for entity_id! macro
= { = "1.0.219", = ["derive"] } # To serialize the `EntityEvent`
= "0.20.1" # For hydrating and building the entity state (optional)
Advanced features
Transactions
All Repository functions exist in 2 flavours.
The _in_op postfix receives an additional argument for the DB connection.
This enables atomic operations across multiple entities.
let mut tx = pool.begin.await?;
users.create_in_op.await?;
accounts.create_in_op.await?;
tx.commit.await?;
Nested Entities
Support for aggregates and child entities:
Testing
The entity style is easily testable. Hydrate from events, mutate, assert.
Documentation
- API Documentation
- Book - In-depth guide and patterns
License
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.