#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-compensation example โ€” Pure Axon saga demo
  • cookbook_saga_compensation_patterns.md โ€” Advanced saga patterns