Rust messaging framework: Mediator, Bus, Outbox, Sagas, Scheduler, Request/Reply.
Hexeract is a server-side messaging framework written in Rust. It unifies an in-process mediator (CQRS), an external message bus (RabbitMQ), a transactional outbox/inbox and a durable message scheduler in a single coherent SDK. The framework relies on Rust's type system and procedural macros to provide compile-time guarantees in place of runtime reflection.
Hexeract is sponsored by Nubster.
⏰ v0.6.0: durable message scheduler shipped. This release adds the time dimension: hexeract-scheduler schedules a message for later (one-shot delay or recurring cron) and drives it through a polling worker with lease-based claiming, bounded backoff with jitter and dead-lettering, dispatching to the mediator, the bus or the outbox. hexeract-scheduler-sql persists schedules on PostgreSQL, MySQL or SQLite, and hexeract scheduler gives operators schema, list, inspect and dead-letter replay commands. All changes to previously published crates are additive. Mediator, middlewares, the #[handler] macro and Bus RabbitMQ stay stable from v0.3.0.
🚧 v0.7.0: Request/Reply in progress, not yet released. hexeract-bus gains a synchronous-over-async RPC surface: the Request trait, RequestClient, the RequestError taxonomy and the RequestRegistry/PendingReply rendezvous, plus RequestHandler and RepliedHandler on the responder side. hexeract-bus-rabbitmq gains the matching exclusive reply inbox, connect_request_client and register_request_handler. The code is complete on the feature branch; the crate versions stay at 0.6.0 and the release ships once every v0.7.0 issue is closed. See the CHANGELOG's [Unreleased] section and docs/explanation/roadmap.md.
| Feature | v0.1.0 | v0.2.0 | v0.3.0 | v0.4.0 | v0.5.0 | v0.6.0 |
|---|---|---|---|---|---|---|
| Transactional outbox (PostgreSQL) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Worker poll loop with SKIP LOCKED |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Fluent builder API | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
hexeract outbox CLI |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Bus core (Message, BusEnvelope, Transport, Handler<M>) |
⏳ | ✅ | ✅ | ✅ | ✅ | ✅ |
RabbitMQ backend (lapin connection pool, publish, consume, retry) |
⏳ | ✅ | ✅ | ✅ | ✅ | ✅ |
Topology types (Exchange, Queue, Binding, RoutingKey) |
⏳ | ✅ | ✅ | ✅ | ✅ | ✅ |
hexeract bus declare / peek / purge CLI |
⏳ | ✅ | ✅ | ✅ | ✅ | ✅ |
In-process CQRS mediator (send, query, publish) |
⏳ | ⏳ | ✅ | ✅ | ✅ | ✅ |
Built-in TracingMiddleware and TimeoutMiddleware |
⏳ | ⏳ | ✅ | ✅ | ✅ | ✅ |
#[handler] attribute macro with verify_handlers() |
⏳ | ⏳ | ✅ | ✅ | ✅ | ✅ |
Multi-database outbox (hexeract-outbox-sql: PostgreSQL, MySQL, SQLite) |
⏳ | ⏳ | ⏳ | ✅ | ✅ | ✅ |
| Delivery reliability (dead-letter, publisher confirms, bounded backoff, dispatch outside tx) | ⏳ | ⏳ | ⏳ | ⏳ | ✅ | ✅ |
| Durable scheduler (delay + cron triggers, SQL backends, sinks, operator CLI) | ⏳ | ⏳ | ⏳ | ⏳ | ⏳ | ✅ |
| Polyglot bus (NATS, Kafka, SQS) | ⏳ v0.9.0 | ⏳ v0.9.0 | ⏳ v0.9.0 | ⏳ v0.9.0 | ⏳ v0.9.0 | ⏳ v0.9.0 |
Request and Reply (Request, RequestClient, RequestHandler) |
⏳ later | ⏳ later | ⏳ later | ⏳ later | ⏳ later | 🚧 unreleased¹ |
| Sagas | ⏳ later | ⏳ later | ⏳ later | ⏳ later | ⏳ later | ⏳ later |
¹ Implemented in code on the request-reply feature branch, pending release; not part of any tagged v0.6.0 artifact.
See the CHANGELOG for the detailed history.
| You need... | Reach for | Crate |
|---|---|---|
| Reliable event delivery tied to your DB transaction | Transactional outbox | hexeract-outbox |
| A SQL outbox on Postgres, MySQL or SQLite | SQL backends | hexeract-outbox-sql |
| In-process command/query dispatch (CQRS) | Mediator | hexeract-mediator |
| Publish and consume over a broker | Message bus | hexeract-bus |
| A RabbitMQ transport | AMQP backend | hexeract-bus-rabbitmq |
| Schedule delayed or recurring messages | Scheduler | hexeract-scheduler |
| A SQL scheduler store on Postgres, MySQL or SQLite | SQL backends | hexeract-scheduler-sql |
| Everything wired together | Umbrella facade | hexeract |
Add the umbrella crate with the outbox-sql-postgres feature to your Cargo.toml (outbox-sql-mysql and outbox-sql-sqlite ship the same surface):
[dependencies]
hexeract = { version = "0.6", features = ["outbox-sql-postgres"] }Power users who prefer a strict SemVer per crate can keep depending on
hexeract-outbox,hexeract-outbox-sql,hexeract-bus,hexeract-bus-rabbitmqetc. directly.
Declare a domain event, a handler and wire a worker:
use std::time::Duration;
use hexeract::core::HandlerContext;
use hexeract::outbox::{Event, Handler, OutboxError, OutboxPublisher};
use hexeract::outbox_sql::{PgOutboxPublisher, PgOutboxWorkerBuilder};
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)]
struct UserRegistered { user_id: Uuid }
impl Event for UserRegistered {
const EVENT_TYPE: &'static str = "users.registered";
}
struct AuditWriter;
impl Handler<UserRegistered> for AuditWriter {
type Error = OutboxError;
async fn handle(&self, event: UserRegistered, _ctx: &HandlerContext) -> Result<(), Self::Error> {
// ... write to audit storage ...
Ok(())
}
}
# async fn run(pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
let publisher = PgOutboxPublisher::new(pool.clone(), "audit_outbox")?;
let worker = PgOutboxWorkerBuilder::new(pool.clone())
.table_name("audit_outbox")
.register_handler::<UserRegistered, _>(AuditWriter)
.poll_interval(Duration::from_millis(50))
.build()?;
let cancel = CancellationToken::new();
let join = tokio::spawn(worker.run(cancel.clone()));
// inside a business use case:
let mut tx = pool.begin().await?;
let event_id = publisher.publish_in_tx(&mut tx, &UserRegistered { user_id: Uuid::new_v4() }).await?;
tx.commit().await?;
println!("published event {event_id}");
cancel.cancel();
join.await??;
# Ok(()) }See docs/getting-started/outbox-quick-start.md for the full integration walkthrough.
Add the umbrella crate with the bus-rabbitmq feature to your Cargo.toml:
[dependencies]
hexeract = { version = "0.6", features = ["bus-rabbitmq"] }Declare a domain message, a handler and wire a publisher plus a worker:
use hexeract::bus::{Handler, Message, Transport};
use hexeract::bus_rabbitmq::{RabbitMqConnection, RabbitMqTransport, RabbitMqWorkerBuilder};
use hexeract::core::HandlerContext;
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)]
struct OrderPlaced { order_id: Uuid }
impl Message for OrderPlaced {
const MESSAGE_TYPE: &'static str = "orders.placed";
}
struct Projector;
impl Handler<OrderPlaced> for Projector {
type Error = hexeract::bus::BusError;
async fn handle(&self, msg: OrderPlaced, _ctx: &HandlerContext) -> Result<(), Self::Error> {
// ... project to read model, forward to downstream system, ...
let _ = msg.order_id;
Ok(())
}
}
# async fn run(uri: &str) -> Result<(), Box<dyn std::error::Error>> {
let transport = RabbitMqTransport::new(uri).await?;
let consumer_conn = RabbitMqConnection::connect(uri).await?;
let worker = RabbitMqWorkerBuilder::new(consumer_conn)
.queue("orders.received")
.register_handler::<OrderPlaced, _>(Projector)
.build()?;
let cancel = CancellationToken::new();
let join = tokio::spawn(worker.run(cancel.clone()));
let message_id = transport
.publish("orders.received", &OrderPlaced { order_id: Uuid::new_v4() })
.await?;
println!("published message {message_id}");
cancel.cancel();
join.await??;
# Ok(()) }In production, declare your topology once at service startup (or out of band through the CLI). The
topology::ensure_topologyhelper and the CLI live for dev convenience; do not call them on the hot path.
The hexeract bus CLI provisions and inspects a broker without writing ad-hoc lapin scripts:
export HEXERACT_BUS_URL=amqp://guest:guest@localhost:5672
# 1. Apply a typed topology described in TOML.
hexeract bus declare --topology crates/hexeract-cli/examples/topology.toml
# 2. Peek the first messages of a queue (non-destructive, requeues each delivery).
hexeract bus peek --queue orders.received --count 5
# 3. Drop every message in a queue (gated by an explicit safety flag).
hexeract bus purge --queue orders.received --yes-i-knowSee the runnable crates/hexeract-examples/examples/03_bus_pubsub.rs for an end-to-end pub/sub against a real RabbitMQ container, and crates/hexeract-cli/examples/topology.toml for the topology file format consumed by hexeract bus declare.
Add the umbrella crate with the mediator feature to your Cargo.toml:
[dependencies]
hexeract = { version = "0.6", features = ["mediator"] }Register a command handler and dispatch through the mediator:
use hexeract::core::{Command, CommandHandler, HandlerContext, HexeractError};
use hexeract::mediator::MediatorBuilder;
struct Greet { name: String }
impl Command for Greet {
type Output = String;
}
struct GreetHandler;
impl CommandHandler<Greet> for GreetHandler {
type Error = HexeractError;
async fn handle(&self, cmd: Greet, _ctx: &HandlerContext) -> Result<String, Self::Error> {
Ok(format!("hello {}", cmd.name))
}
}
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let mediator = MediatorBuilder::new()
.register_command_handler::<Greet, _>(GreetHandler)
.build()?;
let greeting = mediator.send(Greet { name: "world".into() }).await?;
assert_eq!(greeting, "hello world");
# Ok(()) }Queries (Mediator::query) and notifications (Mediator::publish) follow the same pattern. Notifications fan out to every handler registered for the type in registration order; failures are aggregated so siblings keep running. Wire your own Middleware implementations through MediatorBuilder::with_middleware to add tracing, timeouts or any cross-cutting behavior around every dispatch.
Add the umbrella crate with the scheduler-sql-postgres feature to your Cargo.toml (scheduler-sql-mysql and scheduler-sql-sqlite ship the same surface):
[dependencies]
hexeract = { version = "0.6", features = ["scheduler", "scheduler-bus", "scheduler-sql-postgres", "outbox", "bus-rabbitmq"] }Schedule a one-shot reminder and dispatch it onto the bus once it is due:
use hexeract::outbox::Event;
use hexeract::scheduler::{ScheduledMessage, ScheduleStore, Target};
use hexeract::scheduler_sql::PgScheduleStore;
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)]
struct ReminderDue { reminder_id: Uuid, note: String }
impl Event for ReminderDue {
const EVENT_TYPE: &'static str = "reminders.due";
}
# async fn run(pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
let store = PgScheduleStore::new(pool, "scheduled_messages")?;
let reminder = ReminderDue { reminder_id: Uuid::new_v4(), note: "renew your subscription".into() };
let message = ScheduledMessage::delay(
Target::bus("reminders.due"),
SystemTime::now() + Duration::from_secs(60),
&reminder,
)?;
store.insert(&message, 5).await?;
# Ok(()) }A SchedulerBuilder-driven worker polls the store, claims due occurrences under a lease and dispatches them to the mediator, the bus or the outbox, with bounded backoff and dead-lettering on exhausted retries. See docs/getting-started/scheduler-quick-start.md for the full walkthrough, including cron triggers and live schedule control.
Building event-driven services in Rust today means manually wiring a broker client, an outbox table, and an in-process dispatch layer together. Hexeract closes that gap with a single SDK that covers the full shipped surface while keeping each feature independently usable:
- Mediator, dispatch commands to handlers in-process, type-safe and reflection-free.
- Bus, publish and consume over a message broker through a unified transport abstraction.
- Outbox, save business state and outgoing messages atomically in a single database transaction.
- Scheduler, schedule a message for later (one-shot delay or recurring cron) and deliver it durably to the mediator, the bus or the outbox.
The bet behind Hexeract is that Rust's compile-time guarantees turn the outbox pattern from a vigilance discipline into something the type system enforces.
Available today: Mediator, Bus, Outbox/Inbox, the durable Scheduler, and delivery reliability (dead-letter handling, publisher confirms, idempotency).
Request/Reply is implemented in code (unreleased), see the CHANGELOG's
[Unreleased] section. Planned: Sagas. See
docs/explanation/roadmap.md.
To stay focused, the following are explicitly out of scope:
- Not a service mesh. No automatic mTLS or network policies between services. Use a service mesh.
- Not a broker. Hexeract is a client; you keep your existing RabbitMQ, NATS or Kafka.
- Not a standalone workflow engine. Sagas live inside your services, not in a dedicated cluster. Use a dedicated workflow engine when you need that shape.
- Not an event streaming engine. No real-time stream processing. Use a stream-processing engine.
- Rust backend teams building microservices who want a cohesive messaging toolkit instead of stacking incompatible crates.
- Developers migrating to Rust looking for a cohesive messaging SDK.
- Polyglot teams with part of their stack moving to Rust and the need to stay interoperable on a shared bus alongside their Node, Python or Go services.
- API reference: docs.rs for the published crates, and the full workspace rustdoc built on every push to
main, published at https://nubster-opensources.github.io/hexeract/. - Guides: the
docs/index maps quick starts, architecture, concepts, migration and operations guides to their source files.
Contributions are welcome. Please read CONTRIBUTING.md first for the workflow and conventions, and CODE_OF_CONDUCT.md for the community guidelines. For vulnerability reports, see SECURITY.md. For open-ended questions and design conversations, open a thread on the repository discussions.
Stability and versioning are documented in docs/SEMVER_POLICY.md and docs/MSRV_POLICY.md.
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.
See CONTRIBUTING.md for details, including the Contributor License Agreement (CLA).
Copyright © Nubster.