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.