Command and query engines
Both engines live in riverbase_core. Your domain wraps them behind the DomainCommandEngine and
DomainQueryEngine traits, which is what HTTP, transports, and invokers talk to.
EngineContext
Every invocation carries an EngineContext:
- the target namespace;
- correlation and trace IDs;
- the audit actor, profile, tenant, and realm;
- roles and server-resolved claims;
- an optional client idempotency key.
ensure_namespace rejects a context aimed at the wrong domain.
The command engine
CommandEngine<S, A> is generic over a store S: DataStore and an aggregate A: Aggregate<S>. It
holds a closed handler registry, the audit log, the message bus, and the shared store.
Scheduling
Commands run on a ractor actor pool (command_pool_size, alias actor_pool_size). The pool
gives scheduling, isolation per in-flight command, and backpressure. It is not a lock.
Correctness comes from database transactions and optimistic concurrency tokens.
Aggregate vs aggregate root
- Aggregate (
Aggregate/AggregateCore): the command session for one dispatch. It holds the context, the loaded root JSON, the store handle, and buffered effects. - AggregateRoot: a locator (resource, identifier, scope) that says which row an object command opens.
Persistence helpers on the session:
| Method | Semantics |
|---|---|
fetch, find_one, find_all | Reads. No optimistic check. |
create, update, compare_and_swap | Audited writes with optimistic concurrency (_etag) and domain fields |
upsert | Raw replace. Prefer the audited helpers on the command path. |
remove | Delete, subject to the root's declared deletion semantics |
Transactions
The engine begins a CommandUnitOfWork via DataStore::begin_command_transaction and puts it on
AggregateContext::unit_of_work. Every command-path write and audit append uses that handle, so on
Postgres they share one connection and one transaction. A store that claims transactional support
but returns a non-transactional unit fails with CMD-027.
Targets and handlers
| Kind | HTTP | Behavior |
|---|---|---|
Object (and the :link / :hook variants) | :exec | Opens an existing root by resource, identifier, and scope |
Collection | :post | Opens a session with no preloaded root |
TypedCommandHandler<A> declares metadata, an optional payload-derived target, and
handle(payload, &mut A). command_engine! generates one per command.
Idempotency
When a client sends an idempotency key, the engine stores the response with it. A retry with the same key returns the stored response instead of running the command again.
The query engine
A QueryResource declares:
- a logical, public interface: fields, presets, and sort rules;
- a physical binding from fields to a source table and its columns;
- list and item executors;
- a route kind (
QueryorReport); - scope and API-zone metadata;
- a policy requirement and an optional server
scope_policy.
For each request the engine:
- validates the client's
querystatements,sort,limit, andpageagainst the interface; - lowers logical names to physical columns, producing a
DataQuery; - ANDs the tenant filter and the
scope_policyexpression into the serverpolicy_filter; - executes through the registered resource;
- relabels physical columns back to logical field names.
QueryAccess covers List, Item, Meta, and Report.
Scheduling
Queries use a Tokio semaphore (query_pool_size, alias actor_pool_size) to bound concurrent
reads over the shared store. Reads are short and share an immutable context, so they don't need an
actor per request. Commands keep actors because write sessions buffer effects.
Read-only by contract
Queries and reports must not change authoritative or projection state. A report may compute and aggregate. If it needs to write, the operation is a command or a projection handler. See operation classes.
Extension points
| You want | Do this |
|---|---|
| A new command | Add it to the domain's command_engine! |
| A new read model | Add a query_resource! and register it on query_engine! |
| Several slices under one namespace | Use CompositeCommandEngine / CompositeQueryEngine as the domain slot |
| A different read database | Implement DataStore |
| A different audit backend | Implement the LogStore traits and build a DomainLogStore |
| A new transport | Hold Arc<dyn DomainCommandEngine> / Arc<dyn DomainQueryEngine> in an adapter |
Related: Commands crate page · Queries crate page.