Philosophy / Core Paradigm
The Four Pillars
Ranvier's identity is built on four foundational concepts that work together to create a schematic-first, visualizable framework.
Transition Outcome Bus Schematic
1. Transition
A Transition is a pure, composable function that transforms one state into another, potentially failing with a typed error. It's the fundamental unit of computation in Ranvier.
Key Characteristics
Pure: Given the same input, always produces the same output (modulo async I/O)
Typed: Input, output, and error types are explicit
Composable: Transitions can be chained with `.pipe()`, `.fanout()`, `.parallel()`
Testable: Easy to unit test in isolation
Why It Matters
- Type safety: Compiler catches invalid state transitions at build time
- Composition: Build complex workflows by chaining simple transitions
- Visibility: Each transition appears as a node in the Schematic graph
- Testing: Mock inputs/outputs without touching infrastructure
Example
use ranvier::prelude::*;
#[transition]
async fn validate_input(req: Request) -> Outcome<ValidRequest, ValidationError> {
if req.body.is_empty() {
return Outcome::err(ValidationError::EmptyBody);
}
Outcome::ok(ValidRequest::from(req))
}
#[transition]
async fn process(input: ValidRequest) -> Outcome<Response, ProcessError> {
// Business logic here
let result = compute(&input).await?;
Outcome::ok(Response::new(result))
}
// Compose:
let pipeline = Axon::simple::<AppError>()
.pipe(validate_input, process)
.build();2. Outcome
An Outcome is Ranvier's result type that represents success (`ok`) or failure (`err`), with explicit error types. It's similar to `Result<T, E>` but integrates with the Transition system.
Key Characteristics
Explicit errors: Each transition declares its error type
Bus integration: Successful outcomes can store values in the Bus
Schematic metadata: Outcomes carry metadata for visualization
Ergonomic: `?` operator works, plus helper methods like `.map()`, `.and_then()`
Why It Matters
- Error transparency: See all possible failures in the type signature
- Graceful degradation: Handle errors at the right level (transition, pipeline, global)
- Debugging: Outcome metadata helps trace failures through the schematic
Example
// Transition returns Outcome
#[transition]
async fn fetch_user(id: UserId) -> Outcome<User, DatabaseError> {
match db.get_user(id).await {
Ok(user) => Outcome::ok(user),
Err(e) => Outcome::err(DatabaseError::from(e)),
}
}
// Outcome values automatically stored in Bus if marked
#[transition]
async fn enrich_user(user: User) -> Outcome<EnrichedUser, EnrichmentError> {
// 'user' came from Bus (injected automatically)
let profile = fetch_profile(&user).await?;
Outcome::ok(EnrichedUser { user, profile })
}3. Bus
The Bus is a type-safe, in-memory store for sharing state between transitions within a single execution context. Think of it as "dependency injection for data".
Key Characteristics
Type-indexed: Store and retrieve values by their type (like `TypeMap`)
Automatic injection: Transitions can request values from the Bus via parameters
Scoped: Each execution has its own Bus instance (no global state)
Immutable references: Transitions receive `&T` from the Bus (no ownership transfer)
Why It Matters
- Explicit dependencies: Transition signatures show what data they need
- No magic globals: All state is explicit and scoped to the execution
- Testability: Inject mock values into the Bus for testing
- Context propagation: Pass authentication, tenant ID, etc. through the pipeline
Example
#[transition]
async fn authenticate(req: Request) -> Outcome<AuthContext, AuthError> {
let token = extract_token(&req)?;
let auth = validate(token).await?;
// AuthContext automatically stored in Bus for downstream transitions
Outcome::ok(auth)
}
#[transition]
async fn authorize(auth: &AuthContext) -> Outcome<(), AuthError> {
// 'auth' automatically injected from Bus (by type)
if !auth.has_role("admin") {
return Outcome::err(AuthError::Unauthorized);
}
Outcome::ok(())
}
#[transition]
async fn handle_request(auth: &AuthContext, req: &Request) -> Outcome<Response, AppError> {
// Both 'auth' and 'req' injected from Bus
Ok(Response::new(format!("Hello, {}", auth.user_id)))
}4. Schematic
A Schematic is a directed acyclic graph (DAG) representation of your transition pipeline. It's both a runtime execution model and a visual artifact (JSON) that tools like VSCode can render.
Key Characteristics
Nodes: Each transition is a node
Edges: Data flow between transitions
Metadata: Types, error paths, execution stats
Serializable: Export to `schematic.json` for visualization
Why It Matters
- Visibility: See your entire data flow at a glance (in VSCode Circuit view)
- Documentation: The schematic IS the documentation (always up-to-date)
- Debugging: Trace errors through the graph, see which node failed
- Optimization: Identify bottlenecks, parallelize where possible
Example
// Build a schematic
let schematic = Axon::simple::<AppError>()
.pipe(authenticate, authorize, handle_request)
.build();
// Execute
let outcome = schematic.execute(request).await;
// Export to JSON (for VSCode visualization)
let json = schematic.to_json();
std::fs::write("schematic.json", json)?;Next Steps: Now that you understand the core paradigm, explore Why Opinionated Core? to see why Ranvier enforces these
concepts.