Message Bus¶
Introduction¶
The Message Bus decouples the sender of a message from the handler that processes it. Instead of calling a handler directly, you pass a message object to the bus, which looks up the correct handler and dispatches it through a pipeline of cross-cutting behaviors.
- Requests (commands/queries) are dispatched to exactly one handler.
- Events (notifications) are broadcast to zero or more handlers (fan-out).
Relationship to CQRS
CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates read and write models. The message bus provides the infrastructure for CQRS — combined with Event Sourcing, it enables full CQRS+ES architectures.
graph LR
Caller -->|invoke / send| ISender
ISender -->|dispatch| Pipeline[Pipeline Behaviors]
Pipeline --> Handler[Request Handler]
Handler -->|response| ISender
ISender -->|result| Caller
Caller2[Caller] -->|publish| IPublisher
IPublisher -->|fan-out| H1[Event Handler A]
IPublisher -->|fan-out| H2[Event Handler B]
Tip
ISender, IPublisher, and IMessageBus all resolve to the same message bus instance.
Inject only the interface you need — see Interfaces below.
waku's messaging subsystem is inspired by Wolverine (.NET) and integrates with the module system, dependency injection, and extension lifecycle.
Setup¶
Import MessagingModule as a dynamic module in your root module:
MessagingConfig¶
| Option | Type | Default | Description |
|---|---|---|---|
pipeline_behaviors |
Sequence[type[IPipelineBehavior]] |
() |
Global pipeline behaviors applied to every message |
endpoints |
Sequence[EndpointEntry] |
() |
Available message endpoints (see Routing) |
routing |
Sequence[RouteDescriptor \| ModuleRouteDescriptor] |
() |
Route descriptors mapping messages to endpoints (see Routing) |
Passing None (or no argument) to MessagingModule.register() uses the defaults:
MessagingModule is registered as a global module — its providers (message bus, event publisher,
registry) are available to every module in the application without explicit imports.
Interfaces¶
waku provides three message bus interfaces at different levels of access. Inject only the interface you need to enforce the principle of least privilege:
| Interface | Methods | Use when |
|---|---|---|
IMessageBus |
invoke() + send() + publish() |
The component needs full bus access |
ISender |
invoke() + send() |
The component only dispatches commands/queries |
IPublisher |
publish() |
The component only broadcasts events |
IMessageBus extends both ISender and IPublisher:
All three interfaces are automatically registered in the DI container by MessagingModule.
dishka resolves ISender and IPublisher to the same MessageBus instance as IMessageBus.
Dispatch Methods¶
The bus offers three dispatch methods with distinct semantics:
| Method | Returns | Handlers | Description |
|---|---|---|---|
invoke() |
TResponse |
Exactly 1 | In-process request/response. Always inline. |
send() |
None |
Any | Fire-and-forget via endpoint. Raises NoRouteError if no route. |
publish() |
None |
0 or more | Fan-out via endpoints. Silent no-op if no subscribers. |
invoke() — request/response¶
Use invoke() when you need the handler's result. The request travels through the pipeline
and returns a typed response:
send() — fire-and-forget¶
Use send() when you want to dispatch a message without waiting for a response. The message is
always dispatched through an endpoint (the default local queue if no explicit route):
When to use send() vs invoke()
Prefer send() for side-effect-only commands where the caller does not need a result.
Prefer invoke() when the caller depends on the handler's response.
publish() — event fan-out¶
Use publish() to broadcast an event to all registered handlers. If no handlers are registered,
the call is a no-op:
See Events for details on event handlers and publisher strategies.
Complete Example¶
An order placement flow with a command handler and two event handlers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
Fluent chaining
MessagingExtension().bind(...) returns Self, so you can chain multiple bindings in a
single expression.
Exceptions¶
| Exception | Raised when |
|---|---|
HandlerNotFound |
bus.invoke() is called for a request type with no registered handler |
HandlerAlreadyRegistered |
The same handler class is bound to the same message type twice |
MultipleHandlersRegistered |
Multiple handlers registered for an IRequest type |
NoRouteError |
bus.send() is called with no route configured for the message type |
PipelineBehaviorAlreadyRegistered |
The same behavior class is bound to the same message type twice |
Next steps¶
| Topic | Description |
|---|---|
| Requests | Commands, queries, and request handlers |
| Events | Event definitions, handlers, and publishers |
| Pipeline Behaviors | Cross-cutting middleware for request handling |
| Routing & Endpoints | Route messages to background endpoints |
| Message Context | Correlation tracking across message chains |
| Transactions | Unit of work and transactional pipeline behavior |
Further reading¶
- Event Sourcing — event-sourced aggregates, deciders, and projections
- Extension System — lifecycle hooks for application and module lifecycle
- Validation — startup validation and custom rules
- Testing — test utilities and provider overrides