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 local. |
send() |
None |
Exactly 1 | Fire-and-forget. Routable to endpoints. |
publish() |
None |
0 or more | Fan-out to all 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 command without waiting for a response. By default it
runs inline, but it can be routed to an endpoint for background processing:
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_request(...) and .bind_event(...) return Self, so you can
chain multiple bindings in a single expression.
Exceptions¶
| Exception | Raised when |
|---|---|
RequestHandlerNotFound |
bus.invoke() is called for a request type with no registered handler |
RequestHandlerAlreadyRegistered |
A second handler is bound to a request type that already has one |
EventHandlerAlreadyRegistered |
The same handler class is bound to the same event type twice |
PipelineBehaviorAlreadyRegistered |
The same behavior class is bound to the same request 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