Skip to main content

Domains

riverbase_core::domain composes engines into a bounded context and holds the shared runtime they spawn against. For the design rationale see Domains and namespacing.

domain!​

riverbase_core::domain! {
domain TodoDomain {
meta {
namespace: "riverbase.todo",
title: "Todo List",
description: "Reference vertical slice.",
}
command: TodoCommandEngine, // optional for query-only domains
query: TodoQueryEngine,
service: TodoServiceEngine, // optional
routes(api_base) { /* -> axum::Router; needs the `http` feature */ }
spawn(runtime: &DomainRuntime) {
let ctx = Self::engine_context(runtime);
let store = S::init_migrated(&runtime.dbconn, crate::postgres::todo_store_spec())?;
/* spawn engines */
Ok(Self::from_engines(command, query, service))
}
}
}

The generated TodoDomain<S>:

ItemWhat it is
NAMESPACE, TITLE, DESCRIPTIONCompile-time identity constants
spawn(&DomainRuntime)Your constructor, emitted in impl<S: DataStoreInit>
engine_context(&DomainRuntime)The domain's base EngineContext
from_engines(…) / from_query(ctx, query)Assemble the struct from spawned engines
into_arc()Registers the routes block and returns Arc<Self> for mount_domain
command(), query(), service()Typed access to each engine
execute_command, execute_queryCall the domain directly, for tests and workers
impl DomainMetadata, discovery, and the dyn engine slots

DomainRuntime​

Build it once per process and share it across domains:

let runtime = DomainRuntime::from_config(app.config()).await?;

from_config connects Postgres, builds the audit log bundle according to [riverbase.audit_log], connects the message and stream buses from [riverbase.bus], starts the outbox publisher and process-manager worker, and resolves the configured tenant. Its fields (dbconn, logstore, msgbus, stream_bus, invoker, query_invoker, process_manager, process_registry, …) are public, so spawn blocks can pass them to engines.

DomainStoreSpec​

A spec declares what one domain owns in the shared database:

DomainStoreSpec::new(ResourceRegistry::new(["todo", "todo_item"]))
.with_entities(vec![Arc::new(TodoEntity) as Arc<dyn ErasedEntity>])
.with_migrate(run_todo_migrations)
.with_migration_name("todo_examples")

S::init(&runtime.dbconn, spec) builds the store; S::init_migrated also runs with_migrate. Migrations are tracked per crate, in that crate's own schema, so domains never contend over one migrations table.

Engine traits​

TraitImplemented byUsed by
DomainCommandEnginecommand_engine! output (or by hand with domain_trait: manual)HTTP command routes, the command invoker, transports
DomainQueryEnginequery_engine! outputHTTP query routes, the query invoker
DomainServiceEngineYour service typeCapability dispatch by item key
EngineAll of the aboveDiscovery: kind() and items()

Invokers​

CommandInvoker and QueryInvoker map namespaces to engines inside one process. RiverbaseApp::mount_domain registers every mounted domain when the app owns a runtime. Calls carry an invocation mode (delegated user or service capability) and a bounded depth, and always evaluate the target's policy.

Service engines​

A service engine exposes non-CQRS capabilities, such as a liveness ping:

#[async_trait]
impl DomainServiceEngine for TodoServiceEngine {
fn context(&self) -> &EngineContext { &self.ctx }

async fn dispatch(&self, item_key: &str, payload: Value) -> RiverbaseResult<Value> {
match item_key {
"ping" => Ok(json!({ "pong": true, "echo": payload })),
other => Err(NotFoundError::with(
"TDO-SVC-001", "Unknown todo service capability.", format!("unknown service item: {other}"),
)),
}
}
}

Its spawn takes the same arguments as a command engine, so domain! can start it the same way.