- Keep enough typed context inside the application to make correct decisions.
- Return stable, safe transport errors to clients without leaking adapter or database internals.
ddd_cqrs_es gives you typed library errors at the repository and event-store layers. Your application should add one application error boundary on top of those library errors, then map that boundary into HTTP, gRPC, server functions, logs, and metrics.
Layered Error Model
Keep each layer responsible for its own errors:| Layer | Owns | Error shape |
|---|---|---|
| Domain | Business invariants inside Aggregate::handle | Aggregate-specific error type, such as BankAccountError or String in small examples |
| Repository | Command execution and optimistic concurrency | RepositoryError<DomainError, EventStoreError> |
| Event store | Persistence, serialization, connection, and backend failures | EventStoreError |
| Application | Product-facing operation failure | App-specific error, such as CounterAppError |
| Transport | Protocol response shape | REST status/body, ServerFnError, tonic::Status |
String in the shared application service. That erases the distinction between validation, concurrency, unavailable storage, serialization, and internal backend failures.
Library Errors
Repository command paths returnRepositoryError:
EventStoreError preserves broad infrastructure categories:
"connection error: ..." or "event store backend error: ...".
Application Boundary Error
Apps should define a single error type close to the application service. The counter app usesCounterAppError for all Leptos server-function, REST, and Spin gRPC command paths.
RepositoryError into the app boundary error:
Shared Command Service
All command transports should call the same application service. The counter app command service usesAsyncRepository::execute_returning_state, retries expected write conflicts, and returns CounterAppResult<CounterViewDto>.
Transport Mapping
Use one mapping table for every public transport:| App error | REST | gRPC | Server function | Log level |
|---|---|---|---|---|
Validation | 400 Bad Request | InvalidArgument | Public validation message | warn |
Domain | 400 Bad Request or domain-specific 409 | InvalidArgument | Public domain message | warn |
Concurrency | 409 Conflict | Aborted | Retry-safe conflict message | warn |
Store(Connection) | 503 Service Unavailable | Unavailable | Generic storage unavailable message | error |
Configuration | 503 Service Unavailable | Unavailable | Generic configuration message | error |
Store, ReadModel, Projection, Realtime, Serialization, Transport | 500 Internal Server Error | Internal | Generic failure message | error |
REST JSON Errors
REST endpoints should return structured JSON with a stable code:400, and the body contains error.code = "validation".
Leptos Server Functions
Leptos server functions are framework-owned endpoints. Keep them thin:ServerFnError, and return only the public message to the browser.
Spin gRPC
Spin gRPC is served through the HTTP trigger. There is no separate gRPC trigger. Enable it withtransport=grpc or transport=both:
grpcurl from examples/counter-app:
InvalidArgument error with the public message amount must be positive.
Wasmtime currently supports the HTTP transport only. make wasmtime ... transport=grpc and make wasmtime ... transport=both fail fast with a Spin-only transport message.
Tracing and Logs
Initializetracing_subscriber once at the runtime entrypoint, then use structured logs at the application and transport boundaries:
tracing::Span::entered() guard across .await in Leptos server-function futures. The guard is not Send, and server functions require Send futures. Prefer structured events, or use instrumentation patterns that do not keep an entered guard alive across .await.
The counter app Makefile forwards RUST_LOG into Spin and Wasmtime when it is set:
Tests and Verification
Error handling tests should exercise real error values, not fake service stubs:- Construct
RepositoryError::Domain,RepositoryError::Concurrency, andRepositoryError::Store(EventStoreError::Connection)values. - Assert REST status and JSON code/message mapping.
- Assert gRPC
tonic::Codemapping. - Assert server-function conversion uses the public message.
- Compile the runtime combinations that own the transport surface.