Getting Started with Ranvier
Version: 0.17 Updated: 2026-03-04
Prerequisites
- Rust toolchain (1.75+):
rustup,cargo - Git
1. Your First Axon (5 min)
Ranvier models workflows as typed pipelines. Each step is a Transition and they chain together into an Axon.
Create a project
cargo new my-ranvier-app
cd my-ranvier-app
cargo add ranvier-core ranvier-runtime tokio --features tokio/full
cargo add anyhow async-trait serde --features serde/derive
cargo add serde_jsonWrite your first transition
use async_trait::async_trait;
use ranvier_core::prelude::*;
use ranvier_runtime::Axon;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
struct Greeting {
name: String,
message: String,
}
#[derive(Clone)]
struct BuildGreeting;
#[async_trait]
impl Transition<String, Greeting> for BuildGreeting {
type Error = String;
type Resources = ();
async fn run(
&self,
name: String,
_resources: &Self::Resources,
_bus: &mut Bus,
) -> Outcome<Greeting, Self::Error> {
Outcome::Next(Greeting {
name: name.clone(),
message: format!("Hello, {}!", name),
})
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let axon = Axon::<String, String, String>::new("greeting")
.then(BuildGreeting);
let mut bus = Bus::new();
let result = axon.execute("World".to_string(), &(), &mut bus).await;
match result {
Outcome::Next(greeting) => println!("{}", greeting.message),
Outcome::Fault(e) => eprintln!("Error: {}", e),
_ => {}
}
Ok(())
}Key concepts
| Concept | Description |
|---|---|
Transition<From, To> |
A single processing step: takes From, produces To |
Axon |
A pipeline of chained transitions |
Outcome<T, E> |
Result type: Next(T), Fault(E), Branch, Jump, Emit |
Bus |
Type-indexed capability store, passed through the pipeline |
Resources |
Shared dependencies injected into transitions |
Run it
cargo run
# Output: Hello, World!Try the example:
cargo run -p hello-world(from the ranvier workspace)
2. Chaining Transitions (5 min)
Transitions compose with .then():
#[derive(Clone)]
struct ValidateInput;
#[async_trait]
impl Transition<String, String> for ValidateInput {
type Error = String;
type Resources = ();
async fn run(
&self,
input: String,
_resources: &(),
_bus: &mut Bus,
) -> Outcome<String, Self::Error> {
if input.is_empty() {
return Outcome::Fault("input cannot be empty".to_string());
}
Outcome::Next(input)
}
}
let axon = Axon::<String, String, String>::new("pipeline")
.then(ValidateInput)
.then(BuildGreeting);Each .then() appends a step. The output type of one step must match the input type of the next. The compiler enforces this at build time.
Try the example:
cargo run -p typed-state-tree
3. Testing Transitions (10 min)
Transitions are plain async functions — test them directly:
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_validate_rejects_empty() {
let validator = ValidateInput;
let mut bus = Bus::new();
let result = validator.run("".to_string(), &(), &mut bus).await;
assert!(result.is_fault());
}
#[tokio::test]
async fn test_full_pipeline() {
let axon = Axon::<String, String, String>::new("test")
.then(ValidateInput)
.then(BuildGreeting);
let mut bus = Bus::new();
let result = axon.execute("Alice".to_string(), &(), &mut bus).await;
match result {
Outcome::Next(g) => assert_eq!(g.message, "Hello, Alice!"),
other => panic!("Expected Next, got: {:?}", other),
}
}
}Try the example:
cargo run -p testing-patterns(includes 7 unit + integration tests)
4. Custom Error Types (10 min)
Replace String errors with structured types using thiserror:
cargo add thiserroruse thiserror::Error;
#[derive(Debug, Clone, Error, Serialize, Deserialize)]
enum AppError {
#[error("not found: {0}")]
NotFound(String),
#[error("validation failed: {0}")]
Validation(String),
#[error("unauthorized: {0}")]
Unauthorized(String),
}
#[async_trait]
impl Transition<String, User> for FetchUser {
type Error = AppError;
type Resources = ();
async fn run(
&self,
user_id: String,
_resources: &(),
_bus: &mut Bus,
) -> Outcome<User, Self::Error> {
if user_id == "unknown" {
return Outcome::Fault(AppError::NotFound(user_id));
}
Outcome::Next(User { id: user_id, name: "Alice".into() })
}
}Ranvier also provides RanvierError in the core prelude — a serde-compatible error type with Message, NotFound, Validation, and Internal variants.
Try the example:
cargo run -p custom-error-types
5. Using the Bus (5 min)
The Bus is a type-indexed store for passing capabilities through the pipeline:
use ranvier_core::prelude::*;
// Define a capability
#[derive(Clone, Debug)]
struct RequestId(String);
// Insert before execution
let mut bus = Bus::new();
bus.insert(RequestId("req-123".to_string()));
// Read inside a transition
async fn run(&self, input: Input, _res: &(), bus: &mut Bus) -> Outcome<Output, Error> {
let req_id = bus.read::<RequestId>();
match req_id {
Some(id) => println!("Processing request: {}", id.0),
None => println!("No request ID"),
}
Outcome::Next(output)
}The Bus supports insert(), read(), read_mut(), has(), and remove(). Types must be Any + Send + Sync + 'static.
Try the example:
cargo run -p bus-capability-demo
6. Shared Resources (5 min)
For dependencies shared across all transitions (database pools, config), use Resources:
use ranvier_core::transition::ResourceRequirement;
#[derive(Clone)]
struct AppConfig {
api_key: String,
max_retries: u32,
}
impl ResourceRequirement for AppConfig {}
#[async_trait]
impl Transition<Request, Response> for CallExternalApi {
type Error = String;
type Resources = AppConfig;
async fn run(
&self,
req: Request,
config: &Self::Resources,
_bus: &mut Bus,
) -> Outcome<Response, Self::Error> {
println!("Using API key: {}", config.api_key);
// ... use config.max_retries for retry logic
Outcome::Next(response)
}
}
// Pass resources at execution time
let config = AppConfig { api_key: "key".into(), max_retries: 3 };
let result = axon.execute(input, &config, &mut bus).await;Resources must implement ResourceRequirement (a marker trait). The unit type () implements it by default.
7. Resilience: Retry & DLQ (10 min)
Ranvier provides built-in retry with dead-letter queue (DLQ) support:
use ranvier_core::prelude::*;
use ranvier_runtime::Axon;
let axon = Axon::<Input, Input, String>::new("resilient")
.then(UnreliableStep)
.with_dlq_policy(DlqPolicy::RetryThenDlq {
max_attempts: 3,
backoff_ms: 100,
})
.with_dlq_sink(MyDlqSink::new());After max_attempts retries with exponential backoff, failed events are sent to the DLQ sink. Check the Bus for Timeline events to see retry/DLQ activity.
Try the example:
cargo run -p retry-dlq-demo
8. Persistence: Checkpoint & Resume (10 min)
For long-running workflows, use the persistence layer to checkpoint state and resume from failures:
use ranvier_runtime::{
InMemoryPersistenceStore, PersistenceHandle,
PersistenceTraceId, PersistenceAutoComplete,
};
use std::sync::Arc;
let store = Arc::new(InMemoryPersistenceStore::new());
let handle = PersistenceHandle::from_arc(store.clone() as Arc<dyn PersistenceStore>);
// First run — auto_complete=false keeps trace open on fault
let mut bus = ranvier_core::ranvier_bus!(
handle.clone(),
PersistenceTraceId::new("order-123"),
PersistenceAutoComplete(false),
);
let result = axon.execute(input, &(), &mut bus).await;
// Resume from last checkpoint
let trace = store.load("order-123").await?.unwrap();
let cursor = store.resume("order-123", trace.events.last().map(|e| e.step).unwrap_or(0)).await?;Try the example:
cargo run -p state-persistence-demo
9. What to Explore Next
By difficulty
| Level | Examples | Time |
|---|---|---|
| Beginner | hello-world, typed-state-tree, bus-capability-demo |
5 min each |
| Intermediate | routing-demo, flat-api-demo, testing-patterns, custom-error-types |
10-15 min each |
| Advanced | order-processing-demo, retry-dlq-demo, state-persistence-demo, multitenancy-demo |
15-20 min each |
By topic
| Topic | Examples |
|---|---|
| HTTP & Routing | routing-demo, routing-params-demo, flat-api-demo, session-demo |
| Auth & Security | auth-jwt-role-demo, guard-demo |
| Data & Persistence | db-example, persistence-production-demo, ecosystem-redis-demo |
| Observability | observe-http-demo, otel-demo, otel-concept-demo |
| Resilience | retry-dlq-demo, state-persistence-demo |
| Enterprise | synapse-demo, job-scheduler-demo, multitenancy-demo |
Learning paths
- Quick Start: hello-world → typed-state-tree → testing-patterns → custom-error-types → routing-demo
- HTTP Services: routing-params → flat-api → session → multipart-upload → websocket → sse-streaming → openapi → auth-jwt-role
- Advanced Patterns: order-processing → retry-dlq → state-persistence → multitenancy
Run any example
# From the ranvier workspace root
cargo run -p <example-name>