Skip to main content

Domains and namespacing

A domain binds a command engine, a query engine, and optionally a service engine to one namespace, then exposes them to HTTP, transports, and other domains.

The Domain contract​

domain! generates a type that implements the Domain trait:

MemberRole
namespace, title, descriptionIdentity, compiled in as Self::NAMESPACE and friends
command_dynArc<dyn DomainCommandEngine>
query_dynArc<dyn DomainQueryEngine>
service_dynOptional service capability engine
http_routesOptional custom axum routes from the routes(api_base) block
meta, discoveryDerived metadata and the registered command keys and resources

A query-only domain omits command:. The macro fills the slot with a disabled engine, sets capabilities { command: false, query: true }, and emits from_query(ctx, query) for spawn. HTTP mounting then skips command routes.

To merge several engines under one namespace, CompositeCommandEngine and CompositeQueryEngine combine them and reject duplicate keys.

Store binding​

The generated domain is generic over its store, TodoDomain<S>. The command engine is parameterized by S. Query and service engines erase it behind object-safe traits. The spawn block runs inside impl<S: DataStoreInit>, and builds the store from a DomainStoreSpec:

let store = S::init_migrated(&runtime.dbconn, crate::postgres::todo_store_spec())?;

DomainRuntime is the shared infrastructure every domain spawns against:

FieldHolds
dbconnThe Postgres connection
logstoreThe audit log bundle (DomainLogStore)
msgbusThe message bus for integration messages
stream_busThe publish-and-subscribe bus that fans realtime updates (SSE, WebSocket, RxDB) out across replicas
invoker, query_invokerProcess-local registries for calling other domains
process_manager, process_registryDurable multi-step workflows

Naming​

LevelExampleUsed in
Namespaceriverbase.todoEngineContext, routes, discovery, audit, Casbin rows
Command keycreate-todoThe command registry
ResourcetodoThe query registry
Fully qualified nameriverbase.todo/create-todoFqName; cross-boundary metadata
Integration topicriverbase.todo.createdOutbox messages and their consumers

Registries hold local keys. The namespace comes from the domain's execution context. Name things with that split in mind: keys are short and unique within a domain, and namespaces make them unique across a system. riverbase_proc::riverbase_namespace!("…") builds a Namespace from a literal.

Ownership​

A namespace is not by itself a license to write. Each aggregate root and each physical write model has exactly one owning bounded context. Other contexts use the owner's commands, consume its integration messages, or read through read-only ports. They do not register or mutate the owner's entities.

Calling another domain​

CommandInvoker is a process-local map from namespace to DomainCommandEngine. RiverbaseApp::mount_domain registers each mounted domain on it when the app owns a runtime.

Invocation follows an explicit authority model:

  • Delegated-user calls run on behalf of the original caller, rebound to the target domain's context and policy.
  • Service-capability calls run as the service and must name the capability they use.
  • Both modes evaluate the target's policy, and nested invocation depth is bounded.

Orchestration belongs to application handlers and process managers, never to aggregates. A command must not update another domain's state directly. Send a command to it, or publish a message it consumes.

Related: Command and query engines · domain! reference.