Chuyển tới nội dung chính

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:

MethodSemantics
fetch, find_one, find_allReads. No optimistic check.
create, update, compare_and_swapAudited writes with optimistic concurrency (_etag) and domain fields
upsertRaw replace. Prefer the audited helpers on the command path.
removeDelete, 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​

KindHTTPBehavior
Object (and the :link / :hook variants):execOpens an existing root by resource, identifier, and scope
Collection:postOpens 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 (Query or Report);
  • scope and API-zone metadata;
  • a policy requirement and an optional server scope_policy.

For each request the engine:

  1. validates the client's query statements, sort, limit, and page against the interface;
  2. lowers logical names to physical columns, producing a DataQuery;
  3. ANDs the tenant filter and the scope_policy expression into the server policy_filter;
  4. executes through the registered resource;
  5. 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 wantDo this
A new commandAdd it to the domain's command_engine!
A new read modelAdd a query_resource! and register it on query_engine!
Several slices under one namespaceUse CompositeCommandEngine / CompositeQueryEngine as the domain slot
A different read databaseImplement DataStore
A different audit backendImplement the LogStore traits and build a DomainLogStore
A new transportHold Arc<dyn DomainCommandEngine> / Arc<dyn DomainQueryEngine> in an adapter

Related: Commands crate page · Queries crate page.