{"id":50614513,"url":"https://github.com/hops-ops/distributed","last_synced_at":"2026-06-06T07:02:15.689Z","repository":{"id":257935760,"uuid":"872105558","full_name":"hops-ops/distributed","owner":"hops-ops","description":"Distributed is a CQRS and event-sourcing framework for Rust applications that want simple domain models, replayable aggregate history, durable publication, and pluggable infrastructure.","archived":false,"fork":false,"pushed_at":"2026-05-31T21:35:54.000Z","size":1751,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-31T23:09:37.301Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hops-ops.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"AGENTS.md","dco":null,"cla":null}},"created_at":"2024-10-13T19:41:48.000Z","updated_at":"2026-05-31T21:33:20.000Z","dependencies_parsed_at":"2025-02-21T17:38:45.615Z","dependency_job_id":"6f10e707-ef1c-4d47-92d4-d728fd683286","html_url":"https://github.com/hops-ops/distributed","commit_stats":null,"previous_names":["patrickleet/sourced_rust","hops-ops/distributed"],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/hops-ops/distributed","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hops-ops%2Fdistributed","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hops-ops%2Fdistributed/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hops-ops%2Fdistributed/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hops-ops%2Fdistributed/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hops-ops","download_url":"https://codeload.github.com/hops-ops/distributed/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hops-ops%2Fdistributed/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33971111,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-06T02:00:07.033Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":[],"created_at":"2026-06-06T07:02:14.637Z","updated_at":"2026-06-06T07:02:15.677Z","avatar_url":"https://github.com/hops-ops.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Distributed\n\nDistributed is a CQRS and event-sourcing framework for Rust applications that want simple domain models, replayable aggregate history, durable publication, and pluggable infrastructure.\n\nIt keeps your domain model as a plain struct (Plain Old Rust Struct, or PORS), inspired by POCO/POJO, while giving you append-only aggregate event records, replay, snapshots, read models, an outbox, a multi-transport service bus, and a small async command-handler framework.\n\nThe core idea is explicit boundaries: aggregate event records are the write-side source of truth, read models serve queries, and published domain or integration messages are created deliberately through the outbox.\n\nIt is built with stateless vertical and horizontal scaling in cloud-native environments in mind. You can start with a single in-memory service and split it later into partitioned services backed by Postgres and a real broker — without rewriting the domain model.\n\n\u003e **The framework is async-only.** Aggregates, repositories, handlers, the commit\n\u003e path, and the service bus are all `async`. There is no synchronous repository or\n\u003e bus API. Persistence adapters (Postgres, SQLite) and transports (NATS, RabbitMQ,\n\u003e Kafka, Knative) implement the async traits directly with no blocking shims.\n\n## At a Glance\n\n| Capability | What it gives you |\n|---|---|\n| Plain Rust aggregates | Domain state stays in ordinary structs with explicit command methods. |\n| Event-sourced persistence | Append-only `EventRecord`s, replay, optimistic commit, and pluggable async repositories. |\n| Typed macros | `#[sourced]`, `#[digest]`, and `aggregate!()` remove boilerplate while keeping replay explicit. |\n| Snapshots | `#[derive(Snapshot)]` and a snapshot cache speed up hydration for long streams. |\n| Outbox | Durable publication records committed atomically with aggregates. |\n| Read models | Query-optimized relational projections, committed atomically or updated eventually. |\n| Service bus facade | `send`/`listen` (point-to-point) and `publish`/`subscribe` (fan-out) over a swappable transport. |\n| Transports | In-memory, Postgres, NATS JetStream, RabbitMQ, Kafka, and Knative/CloudEvents — one constructor line apart. |\n| Microservice framework | Convention-based async handlers exposed over HTTP, gRPC, the bus, or direct dispatch. |\n| Pluggable infrastructure | Async traits for storage, messaging, read models, snapshots, outbox publishing, and locking. |\n\n## Quick Start\n\nFour steps: write your models, write a command handler, serve it, then swap in\nproduction persistence and transports without touching any of the above.\n\n### 1. Write your models\n\nA domain model is a plain Rust struct with an embedded `Entity`. `#[sourced]` turns\nits command methods into recorded, replayable events; `#[derive(Snapshot)]` adds a\nhydration cache for long streams.\n\n```rust\nuse serde::Deserialize;\nuse distributed::{sourced, Entity, Snapshot};\n\n#[derive(Default, Snapshot)]\nstruct Todo {\n    entity: Entity,\n    user_id: String,\n    task: String,\n    completed: bool,\n}\n\n#[sourced(entity, aggregate_type = \"todo\")]\nimpl Todo {\n    #[event(\"initialized\")]\n    fn initialize(\u0026mut self, id: String, user_id: String, task: String) {\n        self.entity.set_id(\u0026id);\n        self.user_id = user_id;\n        self.task = task;\n    }\n\n    #[event(\"completed\", when = !self.completed)]\n    fn complete(\u0026mut self) {\n        self.completed = true;\n    }\n}\n\n// The command input your handler decodes\n#[derive(Deserialize)]\nstruct CreateTodo {\n    id: String,\n    user_id: String,\n    task: String,\n}\n\n// #[sourced] generates: TodoEvent enum, TryFrom\u003c\u0026EventRecord\u003e, impl Aggregate\n// #[derive(Snapshot)] generates: TodoSnapshot, fn snapshot(), impl Snapshottable\n```\n\n### 2. Write a command handler\n\nEach handler is a module exporting a `COMMAND` name, a `guard`, and an **async**\n`handle`. It loads/creates the aggregate, runs a command, and commits the resulting\nevents — optionally alongside a durable outbox message in the same transaction.\n\n```rust\n// handlers/todo_create.rs\nuse serde_json::{json, Value};\nuse distributed::microsvc::{Context, HandlerError};\nuse distributed::OutboxMessage;\n\nuse super::Repo; // an AggregateRepository\u003c_, Todo\u003e alias\n\npub const COMMAND: \u0026str = \"todo.initialize\";\n\npub fn guard(ctx: \u0026Context\u003cRepo\u003e) -\u003e bool {\n    ctx.has_fields(\u0026[\"id\", \"user_id\", \"task\"])\n}\n\npub async fn handle(ctx: \u0026Context\u003c'_, Repo\u003e) -\u003e Result\u003cValue, HandlerError\u003e {\n    let input = ctx.input::\u003cCreateTodo\u003e()?;\n\n    let mut todo = Todo::default();\n    todo.initialize(input.id.clone(), input.user_id, input.task)?;\n\n    // Record a fact for other services. The outbox row commits atomically with\n    // the aggregate's events. Once a bus is attached (step 3) this `commit`\n    // publishes the row immediately; with no bus it stays pending for a worker.\n    let message = OutboxMessage::domain_event(\"todo.initialized\", \u0026todo)?;\n    ctx.repo().outbox(message).commit(\u0026mut todo).await?;\n\n    Ok(json!({ \"id\": input.id }))\n}\n```\n\n### 3. Serve it\n\nBuild the service fluently from `Service::new()`, register handlers with\n`register_handlers!`, then expose the exact same service over direct dispatch,\nHTTP, gRPC, or the bus. Handlers are written once and are transport-agnostic.\n\n```rust\nuse std::sync::Arc;\nuse distributed::microsvc::{self, Service, Session};\nuse distributed::bus::{InMemoryBus, RunOptions};\nuse distributed::{AggregateBuilder, HashMapRepository, Queueable};\nuse serde_json::json;\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let service = distributed::register_handlers!(\n        Service::new().with_repo(\n            HashMapRepository::new()\n                .queued()\n                .aggregate::\u003cTodo\u003e()\n        ),\n        command handlers::todo_create,\n        command handlers::todo_complete,\n    );\n\n    // Attach a bus and run. `with_bus` closes the loop from step 2: that\n    // `outbox(..).commit(..)` now publishes on commit, and `run` consumes the\n    // registered commands (and events). Same handlers, one line of wiring.\n    service\n        .with_bus(InMemoryBus::new())\n        .run(RunOptions::idempotent())\n        .await?;\n\n    // Alternatives that share the same handlers:\n    //   service.dispatch(\"todo.initialize\", json!({ \"id\": \"todo-1\", .. }), Session::new()).await?; // in-process\n    //   microsvc::serve(Arc::new(service), \"0.0.0.0:3000\").await?;     // HTTP (feature = \"http\")\n    //   microsvc::serve_grpc(Arc::new(service), \"[::1]:50051\").await?; // gRPC (feature = \"grpc\")\n\n    Ok(())\n}\n```\n\n### 4. Swap persistence and transports\n\nEverything above is in-memory. Moving to production is a **constructor change**, not\na handler change — every infrastructure concern is an async trait with an in-memory\ndefault you replace with a durable adapter.\n\n```rust\n// Persistence: HashMapRepository → durable SQL (features \"postgres\" / \"sqlite\")\nlet repo = distributed::PostgresRepository::connect_and_migrate(database_url).await?;\nlet service = distributed::register_handlers!(\n    Service::new().with_repo(repo.queued().aggregate::\u003cTodo\u003e()),\n    command handlers::todo_create,\n    command handlers::todo_complete,\n);\n\n// Transport: InMemoryBus → a real broker. The handlers and the\n// `with_bus(..).run(..)` wiring are unchanged; only this constructor line differs.\n//   let bus = NatsBus::connect(\"nats://localhost:4222\", \"todos\", \"app\").await?;\n//   let bus = PostgresBus::new(pool, \"todos\");\n//   let bus = RabbitBus::connect(\"amqp://localhost:5672/%2f\", \"todos\", \"app\").await?;\n//   let bus = KafkaBus::connect(\"localhost:9092\", \"todos\", \"app\").await?;\nservice.with_bus(bus).run(RunOptions::idempotent()).await?;\n```\n\n| Concern | In-memory default | Swap in for production |\n|---|---|---|\n| Storage | `HashMapRepository` | `PostgresRepository`, `SqliteRepository` |\n| Messaging | `InMemoryBus` | `NatsBus`, `PostgresBus`, `RabbitBus`, `KafkaBus`, `KnativeBus` |\n| Locking | `InMemoryAsyncLockManager` | `PostgresLockManager`, `SqliteLockManager` (durable leases), any `AsyncLockManager` (Redis, …) |\n\nThe rest of this README is the reference guide for each of these pieces.\n\n## Example Conventions\n\nExamples use production-style error propagation. Event methods generated by `#[sourced]` and `#[digest]`, repository calls, and outbox constructors are fallible, so snippets that call them assume a surrounding `async` function that returns a `Result` and use `?` / `.await?`.\n\nComplete runnable examples live under [`tests/`](tests/). Short snippets focus on the API surface and may omit surrounding imports or application-specific types when those are not the point of the example.\n\n## Project Inspiration\n\nDistributed is inspired by the original [sourced](https://github.com/mateodelnorte/sourced) Node.js project by Matt Walters and his accompanying [servicebus](https://github.com/mateodelnorte/servicebus) library for distributed messaging. Patrick Lee Scott, a contributor and maintainer of the original JavaScript/TypeScript versions, brought these concepts to Rust and refactored them for the Rust ecosystem. The bus facade (`send`/`listen` + `publish`/`subscribe`, with per-transport `*Bus` types) mirrors the `servicebus` / `rabbitbus` / `kafkabus` / `knativebus` family.\n\n## Design Goals\n\n- Keep domain objects simple and explicit (Plain Old Rust Structs).\n- Make aggregate event records the source of truth for model state.\n- Make replay predictable and safe.\n- Keep storage and messaging pluggable and testable behind async traits.\n- Make the transport a wiring choice, not a handler change.\n- Add optional queue-based locking for serialized workflows.\n\n## Feature Flags\n\nThe in-memory repository and the service bus facade are part of the core crate and\nalways available. Optional features pull in transports, persistence adapters, and\nnetwork servers.\n\n| Feature | Default | Adds |\n|---|---:|---|\n| `emitter` | Yes | In-process event emission and `#[enqueue]`. |\n| `http` | No | Axum HTTP transport for `microsvc` + the Knative/CloudEvents ingress router. |\n| `grpc` | No | Tonic gRPC transport for `microsvc`. |\n| `postgres` | No | `PostgresRepository` and the Postgres outbox/transport (`PostgresBus`). |\n| `sqlite` | No | `SqliteRepository` async SQL adapter for local persistence and conformance. |\n| `nats` | No | `NatsBus` (NATS JetStream source/publisher). |\n| `rabbitmq` | No | `RabbitBus` (RabbitMQ source/publisher). |\n| `kafka` | No | `KafkaBus` (Kafka source/publisher). |\n\n\u003e The `InMemoryBus` and `PostgresBus` need no broker feature beyond `postgres` for\n\u003e Postgres; the in-memory bus is always available for dev and tests.\n\n## Core Concepts\n\n- **Entity**: Holds the event history. You embed it in your domain structs.\n- **EventRecord**: An immutable aggregate event record with name, payload, sequence, timestamp, and optional metadata. It is replayable model history, not automatically a published domain event.\n- **Aggregate**: A struct that embeds an `Entity` and replays `EventRecord`s. `aggregate_type()` provides the durable stream-identity component for persistence.\n- **Repository / AggregateRepository**: Persists and loads aggregates by event history. The event store is optimized for append and replay; `get`/`commit` are async.\n- **HashMapRepository**: In-memory repository for tests and examples. Implements every async trait (repository, read-model, snapshot, outbox).\n- **SqliteRepository / PostgresRepository**: Durable async SQL adapters (optional features).\n- **QueuedRepository**: Wraps any repository and adds async per-entity queue locking.\n- **EventUpcaster**: A pure, stateless transformation that converts event payloads from one version to another at read time.\n- **Snapshottable**: Opt-in trait for aggregates that produce state snapshot payload DTOs. Use `#[derive(Snapshot)]` to auto-generate the payload struct and trait impl.\n- **OutboxMessage**: A durable publication work item for a domain event, integration event, command, or generic transport message. Supports optional `destination` for point-to-point routing and metadata propagation.\n- **OutboxDispatcher / OutboxWorker**: Drain durable outbox rows and publish them to a transport, sharing one claim → publish → complete path.\n- **ReadModel**: Query-optimized relational projection state for UI/API reads. Read models may be updated atomically with a command or eventually from published messages.\n- **Bus / BusConsumer**: The service bus facade — `send`/`publish` (produce) and `listen`/`subscribe` (consume), implemented by a per-transport `*Bus` type.\n- **microsvc::Service**: Convention-based async command/event handler framework with pluggable transports (HTTP, gRPC, bus, direct dispatch).\n\n## Terminology And CQRS Boundaries\n\nEvent sourcing is the model-level persistence strategy: aggregates record replayable `EventRecord`s when command methods such as `#[event]` (within `#[sourced]`) or `#[digest]` methods succeed. Those records are the write-side history used to hydrate the aggregate.\n\nCQRS is the architectural split between write-side aggregates and query-side read models. Repositories load aggregate event streams by ID for command handling; production business queries should read from `ReadModel` projections shaped for that query.\n\nPublished messages are a separate boundary. An aggregate event record is not automatically a domain event. When other services, projections, or transports need a fact or command, create an `OutboxMessage` and commit it with the aggregate. The outbox payload can represent a domain event, integration event, command, or any other transport message.\n\nThe existing names and serialized fields such as `EventRecord::event_name` remain part of the compatibility contract. Terminology cleanup should clarify usage without renaming stored event records unless a migration path is explicitly designed.\n\n## Pluggable by Default\n\nEvery infrastructure concern in `distributed` follows the same pattern: an **async trait** defines the contract, an **in-memory implementation** ships out of the box for testing and development, and you swap in your own for production.\n\n| Concern | Async trait(s) | In-memory default | Swap in for production |\n|---|---|---|---|\n| Storage | `GetStream` + `TransactionalCommit` | `HashMapRepository` | `PostgresRepository`, `SqliteRepository`, … |\n| Messaging | `Bus` + `BusConsumer` | `InMemoryBus` | `NatsBus`, `PostgresBus`, `RabbitBus`, `KafkaBus`, `KnativeBus` |\n| Read model rows | `ReadModelWritePlanStore` + `RelationalReadModelQueryStore` | `InMemoryReadModelStore` | Postgres, SQLite |\n| Snapshot store | `SnapshotStore` | `InMemorySnapshotStore` | Postgres, SQLite, … |\n| Outbox publishing | `AsyncMessagePublisher` / `OutboxPublisher` | `LogPublisher` | Any transport publisher |\n| Locking | `AsyncLock` + `AsyncLockManager` | `InMemoryAsyncLockManager` | `PostgresLockManager`, `SqliteLockManager` (durable leases), Redis, … |\n\nAll in-memory defaults are `Clone` and `Send + Sync`, so they work in single-task tests and multi-task servers alike. When you're ready for production, implement the trait for your infrastructure and plug it in — handler code does not change.\n\n## The `#[sourced]` Macro\n\nThe `#[sourced]` attribute macro is the recommended way to define event-sourced aggregates. Place it on an impl block and annotate command methods with lowercase, past-tense aggregate event names such as `#[event(\"initialized\")]`. It replaces both `#[digest]` and `aggregate!()`, and auto-generates a typed event enum plus the `Aggregate` impl.\n\nEvent methods are rewritten to return `SourcedResult`, even when the source method omits an explicit return type. Call them with `?` in application code so serialization and event-recording failures are propagated.\n\n### Basic Usage\n\n```rust\nuse distributed::{sourced, Entity};\n\n#[derive(Default)]\nstruct Todo {\n    entity: Entity,\n    user_id: String,\n    task: String,\n    completed: bool,\n}\n\n#[sourced(entity)]\nimpl Todo {\n    #[event(\"initialized\")]\n    fn initialize(\u0026mut self, id: String, user_id: String, task: String) {\n        self.entity.set_id(\u0026id);\n        self.user_id = user_id;\n        self.task = task;\n    }\n\n    #[event(\"completed\", when = !self.completed)]\n    fn complete(\u0026mut self) {\n        self.completed = true;\n    }\n}\n```\n\nThis generates:\n\n```rust\n// Typed event enum with named fields from method parameters\n#[derive(Debug, Clone, PartialEq)]\npub enum TodoEvent {\n    Initialized { id: String, user_id: String, task: String },\n    Completed,\n}\n\nimpl TodoEvent {\n    pub fn event_name(\u0026self) -\u003e \u0026'static str { /* ... */ }\n}\n\n// Convert stored events to typed enum\nimpl TryFrom\u003c\u0026EventRecord\u003e for TodoEvent { /* ... */ }\n\n// Full Aggregate trait impl (entity accessors + replay logic)\nimpl Aggregate for Todo { /* ... */ }\n```\n\n### Durable Stream Identity\n\n`Aggregate::aggregate_type()` provides the type component of a persistence stream's identity (the pair `(aggregate_type, aggregate_id)`). The default uses Rust's type name for development convenience, but **production persistence should set an explicit, stable durable name**:\n\n```rust\n#[sourced(entity, aggregate_type = \"todo\")]\nimpl Todo {\n    // events are stored under the durable stream type \"todo\"\n}\n```\n\n### Using the Typed Event Enum\n\nThe generated enum enables exhaustive matching — if you add or remove an event, the compiler tells you everywhere that needs updating:\n\n```rust\nuse distributed::EventRecord;\n\nfn print_todo_event(record: \u0026EventRecord) -\u003e Result\u003c(), String\u003e {\n    let event = TodoEvent::try_from(record)?;\n    match event {\n        TodoEvent::Initialized { id, user_id, task } =\u003e {\n            println!(\"Todo {} created by {}: {}\", id, user_id, task);\n        }\n        TodoEvent::Completed =\u003e println!(\"Todo completed\"),\n    }\n    Ok(())\n}\n```\n\n### Custom Enum Name\n\n```rust\n#[sourced(entity, events = \"TodoCommand\")]\nimpl Todo {\n    // generates TodoCommand enum instead of TodoEvent\n}\n```\n\n### Versioned Events\n\nCreate events at a specific version for [upcasting](#event-upcasting--versioning):\n\n```rust\ntype InitV1 = (String, String);\ntype InitV2 = (String, String, u8);\n\nfn upcast_init_v1_v2((id, task): InitV1) -\u003e InitV2 {\n    (id, task, 0)\n}\n\n#[sourced(entity, upcasters(\n    (\"initialized\", 1 =\u003e 2, InitV1 =\u003e InitV2, upcast_init_v1_v2),\n))]\nimpl TodoV2 {\n    #[event(\"initialized\", version = 2)]\n    fn initialize(\u0026mut self, id: String, task: String, priority: u8) {\n        // creates events at version 2\n    }\n\n    #[event(\"completed\", when = !self.completed)]\n    fn complete(\u0026mut self) {\n        self.completed = true;\n    }\n}\n```\n\n### Custom Entity Field\n\n```rust\n#[sourced(my_entity)]\nimpl MyAggregate {\n    #[event(\"initialized\")]\n    fn create(\u0026mut self, name: String) {\n        // uses self.my_entity\n    }\n}\n```\n\n### With `enqueue` for Choreography\n\nAdd `enqueue` to `#[sourced]` to automatically queue events for in-process emission alongside digest. Every `#[event]` method both records to the entity stream and enqueues for emission:\n\n```rust\nuse distributed::{sourced, Entity};\nuse distributed::emitter::EntityEmitter;\n\n#[derive(Default)]\nstruct Order {\n    entity: Entity,\n    emitter: EntityEmitter,\n    status: String,\n}\n\n#[sourced(entity, enqueue)]\nimpl Order {\n    #[event(\"initialized\")]\n    fn create(\u0026mut self, order_id: String, customer: String) {\n        self.entity.set_id(\u0026order_id);\n        self.status = \"created\".into();\n    }\n\n    #[event(\"shipped\", when = self.status == \"created\")]\n    fn ship(\u0026mut self) {\n        self.status = \"shipped\".into();\n    }\n}\n```\n\n**Custom emitter field** — when your emitter field isn't named `emitter`:\n\n```rust\n#[sourced(entity, enqueue(my_emitter))]\nimpl Notifier {\n    #[event(\"sent\")]\n    fn send(\u0026mut self, id: String, message: String) {\n        self.entity.set_id(\u0026id);\n        self.message = message;\n    }\n}\n```\n\n## The `#[digest]` Macro and `aggregate!()` Macro\n\nThe `#[digest]` and `aggregate!()` macros are the lower-level building blocks that `#[sourced]` replaces. They're still fully supported and useful when you want more granular control. Like `#[event]` methods, `#[digest]` methods become fallible and should be called with `?`.\n\n### The `#[digest]` Macro\n\n```rust\n// Basic — captures function parameters\n#[digest(\"initialized\")]\nfn initialize(\u0026mut self, id: String, user_id: String, task: String) {\n    self.entity.set_id(\u0026id);\n    self.user_id = user_id;\n    self.task = task;\n}\n\n// Guard conditions — only emit when the condition is true\n#[digest(\"completed\", when = !self.completed)]\nfn complete(\u0026mut self) {\n    self.completed = true;\n}\n\n// Versioned events\n#[digest(\"initialized\", version = 2)]\nfn initialize(\u0026mut self, id: String, task: String, priority: u8) { /* ... */ }\n\n// Custom entity field\n#[digest(my_entity, \"initialized\")]\nfn create(\u0026mut self, name: String) { /* uses self.my_entity */ }\n```\n\n### The `aggregate!` Macro\n\nGenerates the `Aggregate` trait implementation with replay logic:\n\n```rust\naggregate!(Todo, entity, aggregate_type = \"todo\" {\n    \"initialized\"(id, user_id, task) =\u003e initialize,\n    \"completed\"() =\u003e complete(),\n});\n```\n\nWith [upcasters](#event-upcasting--versioning) for event schema evolution:\n\n```rust\ntype InitV1 = (String, String);\ntype InitV2 = (String, String, u8);\n\nfn upcast_initialized_v1_v2((id, task): InitV1) -\u003e InitV2 {\n    (id, task, 0)\n}\n\naggregate!(Todo, entity {\n    \"initialized\"(id, task, priority) =\u003e initialize,\n    \"completed\"() =\u003e complete(),\n} upcasters [\n    (\"initialized\", 1 =\u003e 2, InitV1 =\u003e InitV2, upcast_initialized_v1_v2),\n]);\n```\n\n## Event Metadata\n\nMetadata lets you attach cross-cutting context — correlation IDs, causation IDs, user context, trace spans — to events without changing your domain model.\n\n### Setting Metadata on an Entity\n\nSet metadata on the entity before calling command methods. Every event produced by `#[event]` or `#[digest]` automatically inherits it:\n\n```rust\nlet mut todo = Todo::default();\n\ntodo.entity.set_correlation_id(\"req-abc-123\");\ntodo.entity.set_causation_id(\"cmd-create-todo\");\ntodo.entity.set_meta(\"user_id\", \"u-42\");\n\ntodo.initialize(\"todo-1\".into(), \"user-1\".into(), \"Ship it\".into())?;\n\nassert_eq!(todo.entity.events()[0].correlation_id(), Some(\"req-abc-123\"));\n```\n\nEntity metadata is **transient** — it is not serialized with the entity. It is a request-scoped context you set before each command invocation.\n\n### Propagating Metadata to Outbox Messages\n\nUse `encode_for_entity` to create outbox messages that automatically inherit the entity's metadata context:\n\n```rust\nlet outbox = OutboxMessage::encode_for_entity(\n    format!(\"{}:created\", order.entity.id()),\n    \"order.initialized\",\n    \u0026payload,\n    \u0026order.entity,  // metadata propagates automatically\n)?;\n\nrepo.outbox(outbox).commit(\u0026mut order).await?;\n```\n\nThe metadata flows through the full chain:\n\n```text\nEntity.set_correlation_id(\"req-123\")\n  → #[event] / #[digest] → EventRecord.metadata\n  → encode_for_entity → OutboxMessage.metadata\n  → OutboxDispatcher → transport Message.metadata\n  → subscriber receives the message with correlation_id() == \"req-123\"\n```\n\nFramework-derived metadata (codec, destination, source aggregate) is namespaced under the reserved `x-sourced-` prefix so it cannot be shadowed by user metadata.\n\n### Reading Metadata\n\n```rust\n// On EventRecord (event store)\nevent_record.correlation_id()  // Option\u003c\u0026str\u003e\nevent_record.causation_id()\nevent_record.meta(\"user_id\")\n\n// On OutboxMessage\nmessage.correlation_id()\nmessage.meta(\"trace_id\")\n```\n\n## In-Process Event Choreography (requires `emitter` feature)\n\nThe `emitter` feature (enabled by default) adds in-process event-driven choreography — queue local events during commands and emit them after commit for reactive workflows within a single process.\n\n### With `#[sourced(entity, enqueue)]`\n\nEvery `#[event]` method automatically records to the entity stream (for replay) and enqueues for in-process emission:\n\n```rust\nuse serde::{Deserialize, Serialize};\nuse distributed::{sourced, Entity};\nuse distributed::emitter::EntityEmitter;\n\n#[derive(Default, Serialize, Deserialize)]\nstruct OrderSaga {\n    entity: Entity,\n    #[serde(skip, default)]\n    emitter: EntityEmitter,\n    order_id: String,\n    status: String,\n}\n\n#[sourced(entity, enqueue)]\nimpl OrderSaga {\n    #[event(\"started\")]\n    fn start(\u0026mut self, order_id: String) {\n        self.entity.set_id(\u0026order_id);\n        self.order_id = order_id;\n        self.status = \"started\".into();\n    }\n\n    #[event(\"completed\", when = self.status == \"started\")]\n    fn complete_step(\u0026mut self) {\n        self.status = \"completed\".into();\n    }\n}\n```\n\n### Emitting After Commit\n\nQueued events are held until you explicitly emit them after a successful commit:\n\n```rust\nlet mut saga = OrderSaga::default();\nsaga.start(\"order-1\".into())?;\n\n// Commit the aggregate...\nrepo.commit(\u0026mut saga).await?;\n\n// Then emit queued events to registered listeners\nsaga.emitter.emit_queued();\n```\n\n### Registering Listeners\n\n```rust\nlet shared_state = Arc::new(Mutex::new(Vec::new()));\nlet state = Arc::clone(\u0026shared_state);\n\nsaga.emitter.on(\"started\", move |payload: String| {\n    if let Ok(mut events) = state.lock() {\n        events.push(payload);\n    }\n});\n```\n\nThis pattern is useful for reactive workflows within the same process. For cross-service messaging, use the [Outbox Pattern](#outbox-pattern) and [Service Bus](#service-bus).\n\n## Queued Repository\n\nPer-entity async locking for serialized workflows. `get` acquires the lock, `commit` releases it:\n\n```rust\nuse distributed::{AggregateBuilder, HashMapRepository, Queueable, RepositoryError};\n\nlet repo = HashMapRepository::new().queued().aggregate::\u003cTodo\u003e();\n\nlet Some(mut todo) = repo.get(\"todo-1\").await? else {\n    return Err(RepositoryError::NotFound { id: \"todo-1\".into() });\n}; // locks this ID\n// ... mutate ...\nrepo.commit(\u0026mut todo).await?; // unlocks\n\n// Or release without changes:\nrepo.abort(\u0026todo).await?;\n\n// Read without locking:\nlet _ = repo.peek(\"todo-1\").await?;\n```\n\nBy default, locking is in-memory (`InMemoryAsyncLockManager`) — process-local, lost\non restart. For **cross-process** serialization, back the queue with a durable\nSQLx lease lock (feature `postgres` or `sqlite`). It implements the same\n`AsyncLockManager` trait, so it's a drop-in via `queued_with`:\n\n```rust\nuse distributed::{PostgresLockManager, PostgresRepository};\n\nlet repo = PostgresRepository::connect_and_migrate(\u0026database_url).await?;\n// The `aggregate_locks` lease table is created by the repository's migrations.\nlet locks = PostgresLockManager::new(repo.pool().clone());\nlet todos = repo.queued_with(locks).aggregate::\u003cTodo\u003e();\n```\n\nThe lease records each held key in the `aggregate_locks` table (`SqliteLockManager`\nis the SQLite equivalent). It is a **mutual-exclusion optimization, not a fencing\nguarantee** — the event store's `(aggregate_type, aggregate_id, sequence)` primary\nkey remains the authoritative concurrency boundary. v1 has **no lease renewal**, so\nset the lease TTL above your longest critical section. Tune with `with_lease_ttl`,\n`with_retry_interval`, and `with_max_wait`; reclaim rows from crashed holders with\n`sweep_expired`. Any custom `AsyncLockManager` (e.g. Redis) plugs in the same way.\n\n## Persistent Repositories\n\nThe optional `sqlite` and `postgres` features add async, SQL-backed repositories\nthat implement the same async traits as `HashMapRepository`. They persist aggregate\nevent streams, relational read-model write plans, processed-message marks,\nsnapshots, and outbox rows — staging everything through one SQL transaction when\ncommitted via `CommitBatch`.\n\n```rust\n// SQLite — local persistence and conformance (requires `sqlite`)\nlet repo = distributed::SqliteRepository::connect_and_migrate(\"sqlite::memory:\").await?;\n\n// Postgres — the production SQL event-store path (requires `postgres`)\nlet repo = distributed::PostgresRepository::connect_and_migrate(database_url).await?;\n```\n\n`connect_and_migrate` applies the explicit migrations under `migrations/`. Plain\n`connect` from an existing pool does **not** create tables implicitly, so\napplications can control bootstrap order.\n\nPostgres is the low-ops starter: a single Postgres cluster can back repositories,\nread models, the outbox, **and** the durable transport (`PostgresBus`). See\n[`docs/repositories.md`](docs/repositories.md) for the full guide.\n\n## Outbox Pattern\n\nEach outbox message is a durable delivery row committed alongside your domain entity. Aggregate event records are write-side replay history; they become domain events, integration events, commands, or transport messages only when application code creates an `OutboxMessage` for that purpose.\n\n```rust\nuse distributed::OutboxMessage;\n\nlet mut todo = Todo::default();\ntodo.entity.set_correlation_id(\"req-abc\");\ntodo.initialize(\"todo-1\".into(), \"user-1\".into(), \"Buy milk\".into())?;\n\n// Derives id, snapshot payload, and metadata from the aggregate automatically\nlet message = OutboxMessage::domain_event(\"todo.initialized\", \u0026todo)?;\n\n// Commit both in one repository transaction\nrepo.outbox(message).commit(\u0026mut todo).await?;\n```\n\nFor custom payloads or IDs, use `encode_for_entity`:\n\n```rust\nlet message = OutboxMessage::encode_for_entity(\n    format!(\"{}:init\", todo.entity.id()),\n    \"todo.initialized\",\n    \u0026custom_payload,\n    \u0026todo.entity,\n)?;\n```\n\n### Publishing the Outbox\n\nHow a committed row reaches the bus depends on whether a bus is attached to the\nservice:\n\n- **Bus attached (`service.with_bus(bus)`)** — `repo.outbox(msg).commit(agg)`\n  commits the row, then **immediately** after commit claims it under a short\n  lease and publishes it. A crash before the publish, or a publish failure,\n  leaves the row claimed under that lease; when the lease expires the polling\n  worker takes it.\n- **No bus** — the row is committed `pending` and a worker publishes it.\n\nThe polling worker is the durable backstop in both cases. It is the same\n`OutboxDispatcher` primitive composed with your runtime's timer — run it in the\nservice process or as a separate worker, against the same outbox store:\n\n```rust\nuse distributed::{BusPublisher, OutboxDispatcher};\nuse std::{sync::Arc, time::Duration};\n\nlet dispatcher = OutboxDispatcher::new(\n    repo.outbox_store(),\n    BusPublisher::new(Arc::new(bus)),   // routes commands/events by kind\n    \"outbox-worker-1\",\n    Duration::from_secs(30),            // claim lease\n    5,                                  // max publish attempts\n);\n\nloop {\n    dispatcher.dispatch_batch(100).await?;          // claim → publish → complete\n    tokio::time::sleep(Duration::from_secs(1)).await;\n}\n```\n\nA row completes only after `publish()` resolves `Ok`; an unknown or failed publish\nleaves it retryable (released until the attempt ceiling, then moved to `Failed`).\nClaims use leases, so the immediate path and competing workers never publish the\nsame row concurrently.\n\n## Service Bus\n\nThe service bus is a thin, ergonomic facade over the transport adapters. It exposes\ntwo messaging patterns through two traits:\n\n- **`Bus` (produce)** — `send` a point-to-point command (1:1, competing consumers) or `publish` a fan-out event (1:N).\n- **`BusConsumer` (consume)** — `listen` for commands (competing) or `subscribe` to events (fan-out). `listen`/`subscribe` derive the message names from the service's registered handlers, build the transport's source with the right topology, and run it through the shared runner — handler code never changes.\n\nA concrete `*Bus` implements both, so the **application surface is identical across\ntransports; only the constructor line changes.**\n\n```rust\nuse std::sync::Arc;\nuse distributed::bus::{Bus, BusConsumer, InMemoryBus, RunOptions};\n\n// Built once — handlers are transport-agnostic.\nlet service = Arc::new(build_service());\n\n// Dev/test: in-memory.\nlet bus = InMemoryBus::new();\nbus.send(\"place.bet\", payload).await?;          // point-to-point command (1:1)\nbus.publish(\"seat.reserved\", payload).await?;   // fan-out event (1:N)\nbus.listen(service.clone(), RunOptions::idempotent()).await?;     // competing\nbus.subscribe(service.clone(), RunOptions::idempotent()).await?;  // fan-out\n\n// Production: swap the one constructor line — send/listen/publish/subscribe\n// and the handlers are unchanged.\n//   let bus = NatsBus::connect(\"nats://localhost:4222\", \"orders\", \"app\").await?;\n//   let bus = PostgresBus::new(pool, \"orders\");\n//   let bus = RabbitBus::connect(\"amqp://localhost:5672/%2f\", \"orders\", \"app\").await?;\n//   let bus = KafkaBus::connect(\"localhost:9092\", \"orders\", \"app\").await?;\n```\n\nThis is the low-level facade. For a `microsvc::Service`, the one-call convenience\nis `service.with_bus(bus).run(opts)`: it derives the command names to `listen`\nand the event names to `subscribe` from the registered handlers, and makes\n`repo.outbox(msg).commit(agg)` publish on commit. Drop to `listen` / `subscribe`\n/ `send` / `publish` directly when you need finer control.\n\nPoint-to-point vs fan-out is consistently a **consumer-group/identity** choice in\neach transport's native topology — the same `group` competes, different `group`s\nfan out:\n\n| `*Bus` | Feature | `send` / `listen` (competing) | `publish` / `subscribe` (fan-out) |\n|---|---|---|---|\n| `InMemoryBus` | (always) | named queue, popped once | retained log + per-subscriber cursor |\n| `PostgresBus` | `postgres` | `bus_queue`, `FOR UPDATE SKIP LOCKED` | `bus_log` + `bus_offset` per group (Kafka-style) |\n| `NatsBus` | `nats` | shared durable `{group}_cmd` on the stream | durable `{group}_evt` per group |\n| `RabbitBus` | `rabbitmq` | default exchange → durable queue `{ns}.cmd.{name}` | topic exchange → queue `{ns}.evt.{group}` per group |\n| `KafkaBus` | `kafka` | shared consumer group `{ns}.{group}.cmd` | consumer group per service `{ns}.{group}.evt` |\n| `KnativeBus` | `http` | POST CloudEvent → `{target}-commands` broker ingress | POST → `{source}-events` broker; consume via generated Triggers |\n\n`KnativeBus` implements only `Bus` (produce → broker-ingress POST). It has no\nin-process consume loop: `KnativeBus::manifests(\u0026plan, \u0026subscriptions)` renders the\nrole-based `Broker` + per-name `Trigger` YAML, and the service mounts\n`cloud_events_router` so those Triggers reach `dispatch_message`.\n\n### Idempotency and Failure Policy\n\n`RunOptions::idempotent()` enables idempotent dispatch by default. `RunOptions` also\ncarries a `FailurePolicy` controlling what happens to a **permanent** handler\nfailure — `Retry`, `DeadLetter`, `Park`, `LogAndAck`, or `Stop`:\n\n```rust\nuse distributed::bus::{FailurePolicy, RunOptions};\n\nbus.listen(\n    service.clone(),\n    RunOptions::idempotent().with_failure_policy(FailurePolicy::Stop),\n).await?;\n```\n\nRetryable failures (e.g. transient `NotFound`) are nacked for redelivery; the runner\nnever silently acks a handler error.\n\nSee [`docs/async-transports.md`](docs/async-transports.md) for the full transport\nlayer, the two confirmation thresholds (producer publish vs consumer ack), and the\nlow-level `AsyncMessageSource` / `AsyncMessagePublisher` / `run_source` boundary the\nfacade is built on.\n\n## Microservice Framework (`microsvc`)\n\nThe `microsvc` module provides a convention-based async command/event handler framework. Register handlers on a `Service\u003cD\u003e`, then expose them over HTTP, gRPC, the bus, or direct dispatch.\n\n### Defining a Service\n\nA `Service\u003cD\u003e` is generic over a dependency type `D` that handlers read via `ctx`. Build one fluently from `Service::new()`: add `.with_repo(repo)` for aggregate command handlers, `.with_read_model_store(store)` for projection handlers (chain both when a handler needs both), and `.with_bus(bus)` to consume from / publish to a transport.\n\nHandlers are registered with a fluent builder. `.command(name)` / `.event(name)` start a registration; `.handle(closure)` adds an unguarded handler and `.guarded(guard, closure)` adds a guarded one. The handler closure receives `\u0026Context\u003cD\u003e` and returns a future:\n\n```rust\nuse std::sync::Arc;\nuse distributed::microsvc::{Context, HandlerError, Service, Session};\nuse distributed::{AggregateBuilder, HashMapRepository, Queueable};\nuse serde_json::json;\n\nlet service = Arc::new(\n    Service::new().with_repo(HashMapRepository::new().queued().aggregate::\u003cCounter\u003e())\n        .command(\"counter.initialize\")\n        .handle(|ctx: \u0026Context\u003cRepo\u003e| {\n            let input = ctx.input::\u003cCreateCounter\u003e();\n            async move {\n                let input = input?;\n                let mut counter = Counter::default();\n                counter.create(input.id.clone())?;\n                ctx.repo().commit(\u0026mut counter).await?;\n                Ok(json!({ \"id\": input.id }))\n            }\n        })\n        .command(\"counter.increment\")\n        .handle(|ctx: \u0026Context\u003cRepo\u003e| {\n            let input = ctx.input::\u003cIncrementCounter\u003e();\n            async move {\n                let input = input?;\n                let mut counter = ctx.repo().get(\u0026input.id).await?\n                    .ok_or_else(|| HandlerError::NotFound(input.id.clone()))?;\n                counter.increment(input.amount)?;\n                ctx.repo().commit(\u0026mut counter).await?;\n                Ok(json!({ \"value\": counter.value }))\n            }\n        })\n);\n\n// Direct dispatch\nlet _result = service\n    .dispatch(\"counter.initialize\", json!({ \"id\": \"c1\" }), Session::new())\n    .await?;\n```\n\n### Guards\n\n`.guarded(guard, handler)` runs the guard before the handler — if it returns `false`, the command is rejected:\n\n```rust\nservice\n    .command(\"admin.reset\")\n    .guarded(\n        |ctx: \u0026Context\u003cRepo\u003e| ctx.role() == Some(\"admin\"),\n        |_ctx: \u0026Context\u003cRepo\u003e| async { Ok(json!({ \"reset\": true })) },\n    );\n```\n\n### Handler File Convention\n\nFor larger services, organize handlers into separate files. Each handler module exports a `COMMAND` (or `EVENT` / `EVENTS`) name, a `guard`, and an async `handle`:\n\n```rust\n// src/handlers/counter_create.rs\nuse serde::Deserialize;\nuse serde_json::{json, Value};\nuse distributed::microsvc::{Context, HandlerError};\nuse distributed::OutboxMessage;\n\nuse super::Repo;\nuse crate::models::counter::Counter;\n\npub const COMMAND: \u0026str = \"counter.initialize\";\n\n#[derive(Deserialize)]\nstruct Input { id: String }\n\npub fn guard(ctx: \u0026Context\u003cRepo\u003e) -\u003e bool {\n    ctx.has_fields(\u0026[\"id\"])\n}\n\npub async fn handle(ctx: \u0026Context\u003c'_, Repo\u003e) -\u003e Result\u003cValue, HandlerError\u003e {\n    let input = ctx.input::\u003cInput\u003e()?;\n\n    if ctx.repo().get(\u0026input.id).await?.is_some() {\n        return Err(HandlerError::Rejected(format!(\"counter {} already exists\", input.id)));\n    }\n\n    let mut counter = Counter::default();\n    counter.create(input.id.clone())?;\n\n    let message = OutboxMessage::domain_event(\"counter.initialized\", \u0026counter)?;\n    ctx.repo().outbox(message).commit(\u0026mut counter).await?;\n\n    Ok(json!({ \"id\": input.id }))\n}\n```\n\nRegister them with the `register_handlers!` macro:\n\n```rust\nlet service = distributed::register_handlers!(\n    Service::new().with_repo(HashMapRepository::new().queued().aggregate::\u003cCounter\u003e()),\n    command handlers::counter_create,\n    command handlers::counter_increment,\n);\n```\n\nEvent projection handlers use `EVENT` / `EVENTS` and `event handlers::...` in the same way; inside the handler, `ctx.message()` gives the raw transport `Message` and `ctx.input::\u003cT\u003e()` decodes its payload.\n\n### HTTP Transport (requires `http` feature)\n\nThe `http` feature adds an axum-based HTTP transport. Every registered command becomes a `POST /:command` endpoint. Request headers flow into the `Session`:\n\n```rust\nuse std::sync::Arc;\nuse distributed::microsvc;\n\n// Get an axum Router to compose with other routes\nlet app = microsvc::router(service.clone());\n\n// Or serve directly\nmicrosvc::serve(service, \"0.0.0.0:3000\").await?;\n```\n\nRoutes:\n\n| Method | Path | Description |\n|---|---|---|\n| `POST` | `/:command` | Dispatch a command. Body = JSON input, headers = session variables. |\n| `GET` | `/health` | Health check: `{ \"ok\": true, \"commands\": [\"counter.initialize\", ...] }` |\n\n```bash\ncurl -X POST http://localhost:3000/counter.initialize \\\n  -H 'Content-Type: application/json' \\\n  -H 'x-hasura-user-id: user-42' \\\n  -d '{\"id\": \"c1\"}'\n\ncurl http://localhost:3000/health\n```\n\n### gRPC Transport (requires `grpc` feature)\n\nThe `grpc` feature adds a tonic-based gRPC transport using standard protobuf wire format (no `.proto` file needed):\n\n```rust\n// Get a CommandServiceServer to compose with other tonic routes\nlet grpc_svc = microsvc::grpc_server(service.clone());\n\n// Or serve directly\nmicrosvc::serve_grpc(service, \"[::1]:50051\").await?;\n```\n\n| RPC | Input | Output | Description |\n|---|---|---|---|\n| `Dispatch` | `GrpcRequest` | `GrpcResponse` | Dispatch a command. `input` = JSON string, `session_variables` = metadata map. |\n| `Health` | `HealthRequest` | `HealthResponse` | Health check. |\n\nSession handling mirrors HTTP — gRPC metadata headers are merged with payload `session_variables` (payload takes precedence). Errors are returned inside `GrpcResponse.status` (HTTP-style status codes), keeping client behavior identical across transports.\n\n### Bus Transport\n\nAttach a bus with `service.with_bus(bus)` and drive it with `run(opts)`: it\nderives `listen` (point-to-point commands) and `subscribe` (fan-out events) from\nthe registered handlers, and makes `repo.outbox(msg).commit(agg)` publish on\ncommit. The same `Service` can handle commands from multiple transports\nsimultaneously — HTTP, gRPC, bus, and direct dispatch all share the same handlers\nand repository. For finer-grained control, call the `listen` / `subscribe` facade\nmethods directly. See [Service Bus](#service-bus) above.\n\n### Error Handling\n\n`HandlerError` maps to HTTP-style status codes:\n\n| Variant | Status Code |\n|---|---|\n| `UnknownCommand` | 404 |\n| `DecodeFailed` | 400 |\n| `GuardRejected` | 400 |\n| `Rejected` | 422 |\n| `NotFound` | 404 |\n| `Unauthorized` | 401 |\n| `Repository` | 500 |\n| `Other` | 500 |\n\n## Read Models\n\nRead models are query-optimized relational projections derived from aggregates, event records, or published messages. They are written as declared relational rows using table metadata from `#[derive(ReadModel)]`. Use JSON/JSONB columns for whole-view or semistructured fields.\n\n### Defining a Read Model\n\n```rust\nuse serde::{Deserialize, Serialize};\nuse distributed::ReadModel;\n\n#[derive(Clone, Debug, Serialize, Deserialize, ReadModel)]\n#[table(\"game_views\")]\npub struct GameView {\n    #[id]\n    pub id: String,\n    pub player_name: String,\n    pub score: i32,\n    #[jsonb]\n    pub metadata: serde_json::Value,\n}\n```\n\n### Atomic Commits (Read Model + Aggregate)\n\nWhen the response to a command must include the fully consistent, updated view, commit the aggregate and read model together in one transaction:\n\n```rust\nuse distributed::{ReadModelWritePlanCommitExt, ReadModelWritePlanBuilder};\n\n// Player submits a move\ngame.make_move(player_move)?;\n\n// Build the view from the updated aggregate\nlet view = GameView::from(\u0026game);\n\n// Commit aggregate + view in one transactional batch\nlet mut read_models = ReadModelWritePlanBuilder::new();\nread_models.upsert(\u0026view)?;\nrepo.read_models(read_models).commit(\u0026mut game).await?;\n\n// Return `view` to the client — it reflects the committed state\n```\n\nFor related rows, build the same structured write plan:\n\n```rust\nlet mut read_models = ReadModelWritePlanBuilder::new();\nread_models.upsert(\u0026player_view)?;\nread_models.upsert_related(\u0026player_view, \"weapons\", \u0026weapon_view)?;\nrepo.read_models(read_models).commit(\u0026mut game).await?;\n```\n\nThis is a deliberate consistency tradeoff: the read model is in sync with the aggregate only when the repository can write both in the same transaction boundary (`TransactionalCommit`). For cross-service or cross-database views, use the eventually consistent outbox/projector pattern instead.\n\n### Eventual Projection\n\nDistributed projectors subscribe to published messages and commit read-model rows through a workspace, marking the message processed in the same adapter transaction for SQL idempotency:\n\n```rust\nuse distributed::ReadModelWorkspaceExt;\n\nlet mut workspace = ctx.read_model_store().workspace();\nworkspace.upsert(\u0026row)?;\nworkspace.commit().await?;\n```\n\n### Loading\n\n```rust\nuse distributed::{ReadModelWorkspaceExt, RowKey, RowValue};\n\nlet loaded = repo\n    .workspace()\n    .load::\u003cGameView\u003e(RowKey::new([(\"id\", RowValue::String(\"view-1\".into()))]))\n    .one()\n    .await?;\n```\n\nSee [`docs/read-models.md`](docs/read-models.md) for the full guide, including relational metadata, schema bootstrap, relationship includes, distributed idempotency, and non-goals.\n\n## Snapshots\n\nAs aggregates accumulate events, replaying from scratch gets expensive. The framework keeps aggregate events as the durable source of truth and stores repository snapshots as a rebuildable hydration cache. A snapshot cache record can be deleted and rebuilt from events without changing aggregate correctness.\n\n### Making an Aggregate Snapshottable\n\nAdd `#[derive(Snapshot)]` to your aggregate struct. This generates a state snapshot payload DTO (e.g. `TodoSnapshot`), a `fn snapshot()` method, and the full `impl Snapshottable`:\n\n```rust\nuse distributed::{Entity, Snapshot};\n\n#[derive(Default, Snapshot)]\nstruct Todo {\n    entity: Entity,\n    user_id: String,\n    task: String,\n    completed: bool,\n}\n```\n\nFields with `#[serde(skip)]` (like `emitter: EntityEmitter`) are automatically excluded.\n\n**Custom ID key** — when the entity ID maps to a domain field like `sku`:\n\n```rust\n#[derive(Default, Snapshot)]\n#[snapshot(id = \"sku\")]\nstruct Inventory {\n    entity: Entity,\n    sku: String,\n    available: u32,\n}\n```\n\n**Custom entity field name**:\n\n```rust\n#[derive(Default, Snapshot)]\n#[snapshot(entity = \"my_entity\")]\nstruct Widget {\n    my_entity: Entity,\n    name: String,\n}\n```\n\n### Using Snapshots\n\nChain `.with_snapshots(frequency)` onto any aggregate repository. The frequency is how many events between automatic snapshots:\n\n```rust\nuse distributed::{AggregateBuilder, HashMapRepository, Queueable, RepositoryError};\n\nlet repo = HashMapRepository::new()\n    .queued()\n    .aggregate::\u003cTodo\u003e()\n    .with_snapshots(10); // snapshot every 10 events\n\n// Commit works normally — snapshots are created automatically at the threshold\nlet mut todo = Todo::default();\ntodo.initialize(\"todo-1\".into(), \"user-1\".into(), \"Ship it\".into())?;\nrepo.commit(\u0026mut todo).await?;\n\n// Load transparently restores from the latest snapshot + replays newer events\nlet Some(todo) = repo.get(\"todo-1\").await? else {\n    return Err(RepositoryError::NotFound { id: \"todo-1\".into() });\n};\n```\n\n### How It Works\n\n- **On commit**: If `entity.version().saturating_sub(snapshot_version) \u003e= frequency`, the aggregate's state is serialized via `create_snapshot()` and staged into the same commit transaction as the event append.\n- **On load**: If a usable snapshot cache record exists, the aggregate is restored from its payload and only events with `sequence \u003e snapshot.version` are replayed. Invalid, incompatible, or ahead-of-stream cache records fall back to full replay.\n- **Storage**: Snapshot cache records are stored separately from the event stream, keyed by full stream identity. They carry aggregate type, aggregate ID, covered event version, snapshot payload type/version, codec metadata, cache metadata, and timestamp.\n\n## Event Upcasting / Versioning\n\nEvent schemas evolve over time. When you add a field to an event (e.g., `priority` to `Initialized`), old serialized events in storage can't deserialize into the new type. **Upcasters** solve this: typed functions that transform old event payload shapes into the current format at read time, without modifying stored data.\n\n### Defining an Upcaster\n\nAn upcaster is a plain function that converts a typed payload from one version to the next. The crate handles payload decoding and encoding:\n\n```rust\ntype InitV1 = (String, String);\ntype InitV2 = (String, String, u8);\n\n/// Upcasts Initialized v1 (id, task) → v2 (id, task, priority)\nfn upcast_init_v1_v2((id, task): InitV1) -\u003e InitV2 {\n    (id, task, 0)\n}\n```\n\n### Registering Upcasters\n\nWith `#[sourced]`, add upcasters directly in the attribute:\n\n```rust\n#[sourced(entity, upcasters(\n    (\"initialized\", 1 =\u003e 2, InitV1 =\u003e InitV2, upcast_init_v1_v2),\n))]\nimpl Todo {\n    #[event(\"initialized\", version = 2)]\n    fn initialize(\u0026mut self, id: String, task: String, priority: u8) {\n        self.entity.set_id(\u0026id);\n        self.task = task;\n        self.priority = priority;\n    }\n\n    #[event(\"completed\", when = !self.completed)]\n    fn complete(\u0026mut self) {\n        self.completed = true;\n    }\n}\n```\n\nOld events stored as `(id, task)` at v1 are transparently upcast to `(id, task, 0u8)` at v2 during hydration. New events are created at v2 via the `version = 2` parameter on `#[event]`.\n\n### Chaining Upcasters\n\nUpcasters chain automatically. Each transforms one version to the next (v1→v2→v3):\n\n```rust\n#[sourced(entity, upcasters(\n    (\"initialized\", 1 =\u003e 2, InitV1 =\u003e InitV2, upcast_init_v1_v2),\n    (\"initialized\", 2 =\u003e 3, InitV2 =\u003e InitV3, upcast_init_v2_v3),\n))]\nimpl Todo { /* ... */ }\n```\n\nA v1 event automatically chains through v1→v2→v3; a v2 event only goes through v2→v3; a v3 event passes through unchanged.\n\n### How It Works\n\n- **On hydrate**: Before replaying events, the aggregate's registered upcasters are applied by event name and version.\n- **On snapshot hydrate**: Only post-snapshot events are upcast — the snapshot already contains the current state.\n- **No stored data modified**: Upcasters are read-time transformations.\n- **Zero overhead when unused**: Aggregates with no upcasters take the fast hydration path.\n\n## Project Structure\n\n```\nsrc/\n  aggregate/      # Aggregate trait, hydration, async aggregate repository helpers\n  commit_builder/ # Async transactional batches for aggregates, outbox, and read models\n  emitter/        # In-process event emitter helpers (feature = \"emitter\")\n  entity/         # Entity, event records, metadata, upcasting codecs\n  hashmap_repo/   # In-memory repository (implements every async trait)\n  lock/           # Async lock + lock manager traits, in-memory locks\n  microsvc/       # Command/event handler framework: service, context, session\n    transport/    # Bus facade + adapters (in-memory, postgres, nats, rabbitmq, kafka, knative)\n  outbox/         # Durable outbox message + commit extension\n  outbox_worker/  # Outbox claiming, publishing, workers\n  postgres_repo/  # Postgres async SQL repository (feature = \"postgres\")\n  queued_repo/    # Async queue-based locking repository wrapper\n  read_model/     # Read model store traits, in-memory store, schema metadata\n  snapshot/       # Snapshot store traits, in-memory store, snapshot repository\n  sqlite_repo/    # SQLite async SQL repository (feature = \"sqlite\")\n  table/          # Neutral table/row primitives shared by read models and ops tables\n  lib.rs          # Public exports\ndistributed_macros/\n  src/            # Proc macros: sourced, digest, aggregate, enqueue, ReadModel, Snapshot\ndocs/\n  repositories.md\n  async-transports.md\n  read-models.md\n  postgres-event-store.md\n  research-and-roadmap.md\nmigrations/       # Explicit SQLite and Postgres migrations\ncompose.yaml      # Local postgres / rabbitmq / kafka / nats for integration tests\n```\n\n## Running Tests\n\n```bash\ncargo test                  # default features (`emitter`)\ncargo test --features http\ncargo test --features grpc\nmake test                 # starts compose and runs full local coverage\ncargo test --all-features   # all features; broker tests skip without env vars\n```\n\n### Real-Broker Integration Tests\n\nThe transport adapters have integration tests against real brokers. They are feature-gated and **skip when their env var is unset**:\n\n```bash\ndocker compose up -d   # postgres, rabbitmq, kafka, nats (see compose.yaml)\n\nDATABASE_URL=postgres://sourced:sourced@localhost:5432/distributed \\\n  cargo test --test postgres_transport --features postgres\nNATS_URL=nats://localhost:4222 \\\n  cargo test --test nats_transport --features nats\nAMQP_URL=amqp://guest:guest@localhost:5672/%2f \\\n  cargo test --test rabbitmq_transport --features rabbitmq\nKAFKA_BROKERS=127.0.0.1:9092 \\\n  cargo test --test kafka_transport --features kafka\n```\n\nEach broker has a matching reusable GitHub Actions job (`.github/workflows/integration-*.yaml`) that runs on PRs and on push to `main`.\n\n## Coverage Reporting\n\nThis project uses [`cargo-llvm-cov`](https://github.com/taiki-e/cargo-llvm-cov):\n\n```bash\nrustup component add llvm-tools-preview\ncargo install cargo-llvm-cov\n\ncargo llvm-cov --all-features --summary-only\ncargo llvm-cov --all-features --lcov --output-path lcov.info\n```\n\nCI also publishes `lcov.info` as a workflow artifact and attempts an optional Codecov upload.\n\n## Examples\n\n- `tests/sourced/` — `#[sourced]` macro with typed event enum, `TryFrom`, and aggregate hydration\n- `tests/sourced_upcasting/` — `#[sourced]` with upcasters (v1→v2→v3 chains)\n- `tests/sourced_enqueue/` — `#[sourced(entity, enqueue)]` integrated choreography\n- `tests/sourced_snapshot/` — `#[derive(Snapshot)]` with custom ID keys, `serde(skip)` exclusion, and custom entity fields\n- `tests/snapshots/` — snapshot creation, loading, and partial replay\n- `tests/upcasting/` — event versioning with v1→v2→v3 upcasters, chaining, and snapshot integration\n- `tests/read_models/` — relational read-model projections and atomic commits\n- `tests/distributed_read_model/` — multi-service projection over the bus + persistence matrix\n- `tests/microsvc/` — async handlers, dispatch, session, convention, HTTP, gRPC, and bus transports\n- `tests/sagas/` — saga orchestration and choreography with the outbox pattern\n- `tests/sqlite_repository/`, `tests/postgres_repository/` — durable SQL adapters\n- `tests/transport_conformance/`, `tests/{nats,rabbitmq,kafka,postgres}_transport/`, `tests/knative_cloudevents/` — transport adapters and the shared conformance harness\n\n## License\n\nMIT. See `LICENSE`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhops-ops%2Fdistributed","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhops-ops%2Fdistributed","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhops-ops%2Fdistributed/lists"}