#Core Paradigm โ€” The Four Pillars

Version: 0.33.0 Updated: 2026-03-15 Applies to: ranvier-core, ranvier-macros, ranvier-runtime Category: Philosophy & Architecture


Ranvier's identity is built on four foundational concepts that work together to create a schematic-first, visualizable framework.

#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();

#Outcome

An Outcome is Ranvier's result type that represents success or failure, with explicit error types. It 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 })
}

#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)))
}

#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

  • Why Opinionated Core? โ€” Why Ranvier enforces these concepts
  • Boundary Map โ€” Where core ends and edges begin