Rust library for modeling deciders (command handlers), process managers,
and views (event handlers) in domain-driven, event-sourced, or state-stored
architectures with progressive type refinement.
- Getting Started
- Well-Structured Systems Become Agent-Amplifiable Systems
- Progressive Type Refinement Philosophy
- Educational Purpose
- Two Computation Models
- Threading Modes
- Event Modeling
- Executable Specifications
- Acknowledgments
# Default (multi-threaded, uses Arc + Send + Sync)
cargo build
# Single-threaded (uses Rc, no Send + Sync requirement β better performance in single-threaded runtimes)
cargo build --features single-threaded# All tests β multi-threaded mode (default)
cargo test
# All tests β single-threaded mode
cargo test --features single-threaded
# A specific test by name
cargo test test_name
# A specific test in single-threaded mode
cargo test --features single-threaded test_name
# Show output from passing tests
cargo test -- --nocaptureTip: The
single-threadedfeature swapsArcforRcand removesSend + Syncbounds on all behavioral components. Run both modes in CI to catch issues in each configuration:cargo test && cargo test --features single-threaded
When the structure is right, agents can amplify it. These images show how the pieces connect β from running code to storage:
At runtime, the decider loop is a pure cycle: initial state β decide(command, state) β events are persisted to the event store β evolve(state, event) reconstructs state for the next command. No side effects in the domain logic β all I/O is at the boundary.
Decision Logic β pure deciders that enforce business rules with zero side effects
This library demonstrates how to evolve from general, flexible types to specific, constrained types that better represent real-world information systems. Starting with the most generic interfaces that support all possible type combinations, we progressively add constraints that:
- Increase semantic meaning - Each refinement step adds domain-specific behavior
- Reduce complexity - Constraints eliminate impossible states and invalid operations
- Improve usability - More specific types provide better APIs and clearer intent
- Enable optimizations - Constraints allow for more efficient implementations
This approach mirrors how we model information systems: beginning with broad concepts and iteratively refining them into precise, domain-specific abstractions that capture business rules and invariants.
This library serves as both a practical toolkit and an educational resource for understanding:
- Functional/Data oriented domain modeling patterns in Rust
- Progressive type refinement as a design methodology
- Event-sourced and state-stored computation patterns
// 1. View: Pure state evolution
pub trait ViewTrait<Si, So, Ei> {
fn evolve(&self, state: &Si, event: &Ei) -> So;
fn initial_state(&self) -> Si;
}
// 2. Decide: Decision-making capabilities
pub trait DeciderTrait<C, Si, So, Ei, Eo>: ViewTrait<Si, So, Ei> {
fn decide(&self, command: &C, state: &Si) -> Result<Self::Events, Self::Error>;
}
// 3. Automate: `To-Do` list management
pub trait ProcessTrait<AR, Si, So, Ei, Eo, A>: DeciderTrait<AR, Si, So, Ei, Eo> {
fn react(&self, state: &Si, event: &Ei) -> Self::Actions; // Filtered ToDo list
fn pending(&self, state: &Si) -> Self::Actions; // Complete ToDo list
}
// 4. Implementations with progressive constraints:
impl ViewTrait<S, S, E> for Projection<...> // View/Projection
impl DeciderTrait<C, S, S, E, E> for AggregateDecider<...> // DDD Aggregates
impl DeciderTrait<C, S, S, Ei, Eo> for DCBDecider<...> // DCB
impl ProcessTrait<AR, S, S, E, E, A> for Process<...> // To-Do list processes (manager)
impl ProcessTrait<AR, WorkflowState<T>, WorkflowState<T>, WorkflowEvent<T>, WorkflowEvent<T>, A> for Workflow<...> // Task workflowsState is reconstructed from event history. Events are saved.
Applies to: AggregateDecider, DCBDecider, Process
use fmodel_decider_rust::{AggregateDecider, EventComputationTrait};
// Load historical events from event store
let events = vec![Event::Created, Event::Updated(42)];
// Compute new events based on command and history
let new_events = decider.compute_new_events(&events, &command)?;
// Append new events to event store
append_events_to_store(aggregate_id, new_events);Current state is saved, overwriting the history.
Applies to: AggregateDecider, Process
DCBDeciderdoes not implementStateComputationTraitbecause its input event type (Ei) and output event type (Eo) differ β the output events cannot be folded back into state through the sameevolvefunction.
use fmodel_decider_rust::{AggregateDecider, StateComputationTrait};
// Load current state from database
let current_state = load_state_from_db(entity_id);
// Compute new state based on command
let new_state = decider.compute_new_state(current_state, &command)?;
// Save new state to database
save_state_to_db(entity_id, new_state);Feature-gated thread safety optimization:
# Multi-threaded (default) - Uses Arc, enforces Send + Sync
[dependencies]
fmodel-decider-rust = "0.1.0"
# Single-threaded - Uses Rc, better performance
[dependencies]
fmodel_decider_rust = { version = "0.1.0", features = ["single-threaded"] }The domain is captured as an Event Model β a timeline of commands (blue), events (red), and views (green) across swim lanes (Customer, Admin). Each command/event pair maps to one decider in the codebase.
Event Modeling β capturing the domain as a flow of commands, events, and views
Every decider is tested with a given-when-then specification. Events are folded (evolve) into state, a command (decide) produces new events or an error. The left side shows the success path, the right side the error path β both derived directly from the event model above.
Executable Specifications β translating the model into testable, runnable specs
src/specification.rs provides four fluent DSLs covering every component in the hierarchy:
Supports both event-sourced and state-stored testing for AggregateDecider and Process.
use fmodel_decider_rust::specification::AggregateDeciderTestSpecification;
// Event-sourced: fold history, assert new events
AggregateDeciderTestSpecification::default()
.for_decider(&decider)
.given(vec![AccountEvent::AccountOpened { id: 1, initial_balance: 100 }])
.when(AccountCommand::Deposit { id: 1, amount: 50 })
.then(vec![AccountEvent::MoneyDeposited { id: 1, amount: 50 }]);
// Error path
AggregateDeciderTestSpecification::default()
.for_decider(&decider)
.given(vec![AccountEvent::AccountOpened { id: 1, initial_balance: 50 }])
.when(AccountCommand::Withdraw { id: 1, amount: 100 })
.then_error(AccountError::InsufficientFunds);
// State-stored: provide current state, assert resulting state
AggregateDeciderTestSpecification::default()
.for_decider(&decider)
.given_state(Some(AccountState { id: Some(1), balance: 100 }))
.when(AccountCommand::Deposit { id: 1, amount: 50 })
.then_state(AccountState { id: Some(1), balance: 150 });For DCBDecider where input events (Ei) and output events (Eo) differ across consistency boundaries.
use fmodel_decider_rust::specification::DCBDeciderTestSpecification;
DCBDeciderTestSpecification::<_, MyState, _, _, _, _>::default()
.for_decider(&dcb_decider)
.given(vec![UpstreamEvent::OrderPlaced { order_id: 1, amount: 100 }])
.when(TransformCommand::ProcessOrder { order_id: 1 })
.then(vec![DownstreamEvent::PaymentRequested { order_id: 1, amount: 100 }]);For Projection (read-side views): no command, just fold events into state.
use fmodel_decider_rust::specification::ProjectionTestSpecification;
ProjectionTestSpecification::default()
.for_projection(&projection)
.given(vec![
UserEvent::UserRegistered { id: 1, name: "Alice".into() },
UserEvent::UserUpdated { id: 1, name: "Alice Smith".into() },
])
.then(expected_state);For Process: extends the decider assertions with then_react (actions triggered by the last event) and then_pending (all outstanding actions for the current state). Assertions are chainable.
use fmodel_decider_rust::specification::ProcessTestSpecification;
ProcessTestSpecification::default()
.for_process(&process)
.given(vec![
TaskEvent::TaskAdded { id: 1, title: "Task 1".into() },
TaskEvent::TaskInProgress { id: 1 },
])
.when(TaskActionResult::TaskStarted { id: 1 })
.then(vec![TaskEvent::TaskInProgress { id: 1 }])
.then_react(vec![TaskAction::NotifyManager { task_id: 1 }])
.then_pending(vec![]);- Inspired by fmodel-decider and progressive type refinement philosophy
- Special credits to
JΓ©rΓ©mie Chassaingfor his research andAdam Dymitrukfor Event Modeling.
Created with β€οΈ by Fraktalio
