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:
| Member | Role |
|---|---|
namespace, title, description | Identity, compiled in as Self::NAMESPACE and friends |
command_dyn | Arc<dyn DomainCommandEngine> |
query_dyn | Arc<dyn DomainQueryEngine> |
service_dyn | Optional service capability engine |
http_routes | Optional custom axum routes from the routes(api_base) block |
meta, discovery | Derived 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:
| Field | Holds |
|---|---|
dbconn | The Postgres connection |
logstore | The audit log bundle (DomainLogStore) |
msgbus | The message bus for integration messages |
stream_bus | The publish-and-subscribe bus that fans realtime updates (SSE, WebSocket, RxDB) out across replicas |
invoker, query_invoker | Process-local registries for calling other domains |
process_manager, process_registry | Durable multi-step workflows |
Naming
| Level | Example | Used in |
|---|---|---|
| Namespace | riverbase.todo | EngineContext, routes, discovery, audit, Casbin rows |
| Command key | create-todo | The command registry |
| Resource | todo | The query registry |
| Fully qualified name | riverbase.todo/create-todo | FqName; cross-boundary metadata |
| Integration topic | riverbase.todo.created | Outbox 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.