#Axum + Ranvier Hybrid Guide
Version: 0.43.0 | Updated: 2026-03-28 | Applies to: ranvier-core, ranvier-runtime | Category: Integration
#Overview
The hybrid pattern: Axum serves HTTP, Ranvier processes complex logic.
Use Axum for routing, extractors, and Tower middleware. Use Ranvier when an endpoint involves multi-step business logic that benefits from typed pipelines, saga compensation, or schematic inspection.
#1. Basic Integration
Call a Ranvier Axon pipeline from an Axum handler:
use axum::{Router, Json, routing::post, http::StatusCode, response::IntoResponse};
use ranvier_core::prelude::*;
use ranvier_runtime::Axon;
#[transition]
async fn validate_order(input: OrderRequest) -> Outcome<ValidatedOrder, String> {
if input.items.is_empty() {
return Outcome::Fault("Order must have at least one item".into());
}
Outcome::Next(ValidatedOrder { /* ... */ })
}
async fn create_order_handler(
Json(request): Json<OrderRequest>,
) -> impl IntoResponse {
let pipeline = Axon::typed::<OrderRequest, String>("create-order")
.then(validate_order)
.then(process_payment)
.then(confirm_order);
let mut bus = Bus::new();
match pipeline.execute(request, &(), &mut bus).await {
Outcome::Next(result) => (StatusCode::CREATED, Json(result)).into_response(),
Outcome::Fault(err) => {
(StatusCode::BAD_REQUEST, Json(serde_json::json!({"error": err}))).into_response()
}
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/api/orders", post(create_order_handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}#2. Saga Compensation in Axum
The most powerful hybrid pattern โ Axum handles HTTP, Ranvier manages the saga:
use ranvier_core::saga::SagaPolicy;
async fn process_order_saga(
Json(request): Json<OrderRequest>,
) -> impl IntoResponse {
let saga = Axon::typed::<OrderRequest, String>("order-saga")
.with_saga_policy(SagaPolicy::Enabled)
.then(validate_order)
.then_compensated(reserve_inventory, release_inventory)
.then_compensated(charge_payment, refund_payment)
.then_compensated(confirm_shipping, cancel_shipment)
.then(complete_order);
let mut bus = Bus::new();
match saga.execute(request, &(), &mut bus).await {
Outcome::Next(result) => (StatusCode::OK, Json(result)).into_response(),
Outcome::Fault(err) => {
// Compensation already ran โ inventory released, payment refunded
(StatusCode::UNPROCESSABLE_ENTITY, Json(serde_json::json!({
"error": err,
"compensated": true
}))).into_response()
}
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}When charge_payment fails, Ranvier automatically runs release_inventory in reverse.
The Axum handler only sees the final result โ success or compensated failure.
#3. Sharing Axum State with Ranvier Bus
Bridge Axum's State into the Ranvier Bus:
use axum::extract::State;
use std::sync::Arc;
struct AppState {
db: Arc<DatabasePool>,
}
async fn create_user_handler(
State(state): State<Arc<AppState>>,
Json(request): Json<CreateUserRequest>,
) -> impl IntoResponse {
let pipeline = Axon::typed::<CreateUserRequest, String>("create-user")
.then(validate_input)
.then(check_duplicates)
.then(insert_user);
let mut bus = Bus::new();
bus.insert(state.db.clone()); // Axum state โ Ranvier Bus
match pipeline.execute(request, &(), &mut bus).await {
Outcome::Next(user) => (StatusCode::CREATED, Json(user)).into_response(),
Outcome::Fault(err) => {
(StatusCode::BAD_REQUEST, Json(serde_json::json!({"error": err}))).into_response()
}
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
// In the transition, read from Bus:
#[transition]
async fn insert_user(input: ValidatedUser, bus: &mut Bus) -> Outcome<User, String> {
let db = bus.get_cloned::<Arc<DatabasePool>>().unwrap();
let user = db.insert_user(&input).await.map_err(|e| e.to_string())?;
Outcome::Next(user)
}#4. When to Use What
#Use Ranvier standalone (`Ranvier::http()`) when:
- Starting a new project where most endpoints have complex logic
- You want unified Schematic extraction across all endpoints
- You want built-in Guards (CORS, auth, rate limit) without Tower
#Use Axum + Ranvier hybrid when:
- You have an existing Axum codebase and want to add complex workflows
- Only some endpoints need multi-step pipelines โ simple CRUD stays in Axum
- You want Axum's Tower middleware ecosystem for cross-cutting concerns
- Your team is already familiar with Axum patterns
#Use Axum alone when:
- All endpoints are simple CRUD (request โ DB โ response)
- Per-request overhead matters (proxy, gateway)
- You don't need saga compensation, screening, or schematic inspection
#5. Trade-offs
| Aspect | Benefit | Limitation |
|---|---|---|
| Extractors | Axum's type-safe extractors (Json, State, Path) | Manual bridging to Bus |
| Tower Middleware | Full Tower ecosystem via Axum layers | Not visible in Ranvier Schematic |
| Saga | Ranvier handles compensation automatically | Extra setup for Axon pipelines |
| Testing | Unit test transitions independently | Integration tests need both Axum + Ranvier |
#See Also
- When to Use Ranvier vs Axum โ Decision table and flowchart
saga-compensationexample โ Pure Axon saga democookbook_saga_compensation_patterns.mdโ Advanced saga patterns