Skip to content

Hexeract

Rust messaging framework: Mediator, Bus, Outbox, Sagas, Scheduler, Request/Reply.

crates.io docs.rs CI MSRV License: MIT OR Apache-2.0 Status Made with Rust

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.

Status

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.

What do you need?

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

Quick start

Outbox (PostgreSQL)

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-rabbitmq etc. 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.

Bus (RabbitMQ)

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_topology helper 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-know

See 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.

Mediator (in-process)

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.

Scheduler (delayed and recurring messages)

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.

Why Hexeract

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.

Roadmap

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.

What Hexeract is not

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.

Audience

  • 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.

Documentation

Contributing

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.

License

Licensed under either of:

at your option.

Contribution

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.

About

Rust messaging framework: Mediator, Bus, Outbox, Sagas, Scheduler, Request/Reply.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages