{"id":34569922,"url":"https://github.com/galoymoney/es-entity","last_synced_at":"2026-04-20T20:01:33.595Z","repository":{"id":305178542,"uuid":"1021975021","full_name":"GaloyMoney/es-entity","owner":"GaloyMoney","description":"Framework for persisting event-sourced entities in PostgreSQL.","archived":false,"fork":false,"pushed_at":"2026-04-08T15:54:53.000Z","size":1260,"stargazers_count":14,"open_issues_count":8,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-08T17:33:20.907Z","etag":null,"topics":["event-sourcing","postgresql","rust","sqlx"],"latest_commit_sha":null,"homepage":"https://galoymoney.github.io/es-entity/index.html","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GaloyMoney.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-18T08:48:15.000Z","updated_at":"2026-04-08T15:55:01.000Z","dependencies_parsed_at":null,"dependency_job_id":"67fa6c8c-aba8-4936-bfd7-08a02fe7a162","html_url":"https://github.com/GaloyMoney/es-entity","commit_stats":null,"previous_names":["galoymoney/es-entity"],"tags_count":62,"template":false,"template_full_name":null,"purl":"pkg:github/GaloyMoney/es-entity","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GaloyMoney%2Fes-entity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GaloyMoney%2Fes-entity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GaloyMoney%2Fes-entity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GaloyMoney%2Fes-entity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GaloyMoney","download_url":"https://codeload.github.com/GaloyMoney/es-entity/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GaloyMoney%2Fes-entity/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32063458,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-20T11:35:06.609Z","status":"ssl_error","status_checked_at":"2026-04-20T11:34:48.899Z","response_time":94,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["event-sourcing","postgresql","rust","sqlx"],"created_at":"2025-12-24T09:16:42.206Z","updated_at":"2026-04-20T20:01:33.584Z","avatar_url":"https://github.com/GaloyMoney.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# es-entity\n\n[![Crates.io](https://img.shields.io/crates/v/es-entity)](https://crates.io/crates/es-entity)\n[![Documentation](https://docs.rs/es-entity/badge.svg)](https://docs.rs/es-entity)\n[![Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)\n[![Unsafe Rust forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)\n\nAn Event Sourcing Entity Framework for Rust that simplifies building event-sourced applications with PostgreSQL. \n\nThe framework enables writing Entities that are:\n- **Event Sourced** - Entities are hydrated via event projection.\n- **Idempotent** - Built-in guards against duplicate operations\n- **Testable** - Clean separation between domain logic and persistence\n\nPersisted to postgres with:\n- **Minimal boilerplate** - Derive macros generate repository methods automatically\n- **Compile-time verified** - All SQL queries are checked at compile time via [sqlx](https://github.com/launchbadge/sqlx)\n- **Optimistic concurrency** - Automatic detection of concurrent updates via event sequences\n- **Pagination** - Cursor-based pagination out of the box\n\n[Book](https://galoymoney.github.io/es-entity/index.html) |\n[API Docs](https://docs.rs/es-entity/latest/es_entity/) |\n[GitHub repository](https://github.com/GaloyMoney/es-entity) |\n[Cargo package](https://crates.io/crates/es-entity)\n\n_Free of any unsafe code`#![forbid(unsafe_code)]` to ensure everything is implemented in 100% safe Rust._\n\n## Quick Example\n\n### Entity\nFirst you need your entity:\n\n```rust\n// Define your entity ID (can be any type fulfilling the traits).\nes_entity::entity_id! { UserId }\n\n// Define your events\n#[derive(EsEvent, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\n#[es_event(id = \"UserId\")]\npub enum UserEvent {\n    Initialized { id: UserId, name: String },\n    NameUpdated { name: String },\n}\n\n// Define your entity\n// derive_builder::Builder is optional but useful for hydrating\n#[derive(EsEntity, Builder)]\n#[builder(pattern = \"owned\", build_fn(error = \"EntityHydrationError\"))]\npub struct User {\n    pub id: UserId,\n    pub name: String,\n    // Container for your events\n    events: EntityEvents\u003cUserEvent\u003e,\n}\n\nimpl User {\n    // Mutations append events\n    pub fn update_name(\u0026mut self, new_name: impl Into\u003cString\u003e) -\u003e Idempotent\u003c()\u003e{\n        let name = new_name.into();\n        // Check whether the event was already recorded\n        idempotency_guard!(\n            self.events.iter().rev(),\n            // Return Idempotent::AlreadyApplied if this pattern hits\n            already_applied: UserEvent::NameUpdated { name: existing_name } if existing_name == \u0026name,\n            // Stop searching here\n            resets_on: UserEvent::NameUpdated { .. }\n        );\n        self.name = name.clone();\n        self.events.push(UserEvent::NameUpdated { name });\n        Idempotent::Executed(())\n    }\n}\n\n// TryFromEvents hydrates the user entity from persisted events.\nimpl TryFromEvents\u003cUserEvent\u003e for User {\n    fn try_from_events(events: EntityEvents\u003cUserEvent\u003e) -\u003e Result\u003cSelf, EntityHydrationError\u003e {\n        // Using derive_builder::Builder to project the current state\n        // while iterating over the persisted events\n        let mut builder = UserBuilder::default();\n        for event in events.iter_all() {\n            match event {\n                UserEvent::Initialized { id, name } =\u003e {\n                    builder = builder.id(*id).name(name.clone());\n                }\n                UserEvent::NameUpdated { name } =\u003e {\n                    builder = builder.name(name.clone());\n                }\n            }\n        }\n        builder.events(events).build()\n    }\n}\n```\n\n### Persistence\n\nSetup your database - each entity needs 2 tables.\n\n```sql\n-- Index table for queries\nCREATE TABLE users (\n  id UUID PRIMARY KEY,\n  created_at TIMESTAMPTZ NOT NULL,\n  name VARCHAR UNIQUE  -- Add columns you want to query by\n);\n\n-- Event storage table\nCREATE TABLE user_events (\n  id UUID NOT NULL REFERENCES users(id),\n  sequence INT NOT NULL,\n  event_type VARCHAR NOT NULL,\n  event JSONB NOT NULL,\n  context JSONB DEFAULT NULL,\n  recorded_at TIMESTAMPTZ NOT NULL,\n  UNIQUE(id, sequence)\n);\n```\nRepository methods are generated:\n```rust\n// Define your repository - all CRUD operations are generated!\n#[derive(EsRepo)]\n#[es_repo(entity = \"User\", columns(name(ty = \"String\")))]\npub struct Users {\n    pool: PgPool,\n}\n\n// // Generated Repository fns:\n// impl Users {\n//     // Create operations\n//     async fn create(\u0026self, new: NewUser) -\u003e Result\u003cUser, UserCreateError\u003e;\n//     async fn create_all(\u0026self, new: Vec\u003cNewUser\u003e) -\u003e Result\u003cVec\u003cUser\u003e, UserCreateError\u003e;\n//\n//     // Query operations\n//     async fn find_by_id(\u0026self, id: UserId) -\u003e Result\u003cUser, UserFindError\u003e;\n//     async fn find_by_name(\u0026self, name: \u0026str) -\u003e Result\u003cUser, UserFindError\u003e;\n//\n//     // Update operations\n//     async fn update(\u0026self, entity: \u0026mut User) -\u003e Result\u003c(), UserModifyError\u003e;\n// \n//     // Paginated listing\n//     async fn list_by_id(\u0026self, args: PaginatedQueryArgs, direction: ListDirection) -\u003e PaginatedQueryRet;\n//\n//     // etc\n// }\n```\n\n### Usage\n```rust\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let pool = PgPool::connect(\"postgres://localhost/myapp\").await?;\n    let users = Users { pool };\n    \n    // Create a new user\n    let user = users.create(NewUser {\n        id: UserId::new(),\n        name: \"Alice\".to_string(),\n    }).await?;\n    \n    // Query by indexed columns\n    let alice = users.find_by_name(\"Alice\").await?;\n    \n    // Update with automatic idempotency\n    let mut user = users.find_by_id(user.id).await?;\n    if user.update_name(\"Alice Cooper\").did_execute() {\n        users.update(\u0026mut user).await?;\n    }\n    \n    Ok(())\n}\n```\n\n## Getting Started\n\n### Installation\n\nAdd to your `Cargo.toml`:\n\n```toml\n[dependencies]\nes-entity = \"0.9\"\nsqlx = \"0.8.3\" # Needs to be in scope for entity_id! macro\nserde = { version = \"1.0.219\", features = [\"derive\"] } # To serialize the `EntityEvent`\nderive_builder = \"0.20.1\" # For hydrating and building the entity state (optional)\n```\n\n## Advanced features\n### Transactions\n\nAll Repository functions exist in 2 flavours.\nThe `_in_op` postfix receives an additional argument for the DB connection.\nThis enables atomic operations across multiple entities.\n\n```rust\nlet mut tx = pool.begin().await?;\nusers.create_in_op(\u0026mut tx, new_user).await?;\naccounts.create_in_op(\u0026mut tx, new_account).await?;\ntx.commit().await?;\n```\n### Nested Entities\n\nSupport for aggregates and child entities:\n\n```rust\n#[derive(EsEntity)]\npub struct Order {\n    pub id: OrderId,\n\n    // Child entity - auto implements Parent\u003cOrderItem\u003e for Order\n    #[es_entity(nested)]\n    items: Nested\u003cOrderItem\u003e,\n\n    events: EntityEvents\u003cOrderEvent\u003e,\n}\n\n#[derive(EsRepo, Debug)]\n#[es_repo(\n    entity = \"OrderItem\",\n    // Child repo marks the parent foreign key\n    columns(order_id(ty = \"OrderId\", update(persist = false), parent))\n)]\nstruct OrderItems {\n    pool: PgPool,\n}\n\n#[derive(EsRepo)]\n#[es_repo(\n    entity = \"Order\",\n)]\npub struct Orders {\n    pool: PgPool,\n\n    // Parent repo owns the child repo\n    #[es_repo(nested)]\n    items: OrderItems,\n}\n```\n\n## Testing\n\nThe entity style is easily testable. Hydrate from events, mutate, assert.\n\n```rust\n#[cfg(test)]\nmod tests {\n    use super::*;\n    \n    fn test_user(id: UserId) -\u003e User {\n        let events = EntityEvents::init(\n            id,\n            [UserEvent::Initialized { \n                id,\n                name: \"Alice\".to_string() \n            }],\n        );\n        \n        User::try_from_events(events).unwrap();\n    }\n\n    #[test]\n    fn test_user_update() {\n        let mut user = test_user(UserId::new());\n        assert_eq!(user.update_name(\"Bob\"), Idempotent::Executed(()));\n        assert_eq!(user.update_name(\"Bob\"), Idempotent::AlreadyApplied(()));\n    }\n}\n```\n\n## Documentation\n\n- [API Documentation](https://docs.rs/es-entity)\n- [Book](https://galoymoney.github.io/es-entity) - In-depth guide and patterns\n\n## License\n\nThis project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgaloymoney%2Fes-entity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgaloymoney%2Fes-entity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgaloymoney%2Fes-entity/lists"}