Why Commands Return Events?
Why command handlers return events instead of calling eventBus.dispatch() directly.
The Decision
In noddde, command handlers return the event(s) they produce as a return value. The framework handles persistence and publishing.
commands: {
AuthorizeTransaction: (command, state, infra) => {
return { name: "TransactionAuthorized", payload: { ... } };
},
},The Problem
In many frameworks, command handlers call this.apply() or eventBus.dispatch() directly:
// Traditional approach (NOT how noddde works)
class BankAccount {
authorizeTransaction(command, eventBus) {
const event = new TransactionAuthorized(command);
eventBus.dispatch(event); // Side effect!
this.state.balance -= command.amount; // Mutation!
}
}This creates problems:
- Hidden side effects — The handler modifies external state (event bus, aggregate state)
- Ordering issues — Events are published before persistence; if persistence fails, events were already dispatched
- Difficult testing — Need to mock the event bus and verify calls
- Mixed concerns — The handler decides AND publishes AND mutates
Alternatives Considered
this.apply(event)— The Axon/Eventuate pattern where the handler calls a framework method- Event bus injection — Handler receives
eventBusin infrastructure and callsdispatch - Callback pattern — Handler receives a
publishcallback
Why This Approach
Returning events makes the handler a pure decision function:
// The handler only decides WHAT happened
commands: {
AuthorizeTransaction: (command, state) => {
if (state.availableBalance < command.payload.amount) {
return { name: "TransactionDeclined", payload: { ... } };
}
return { name: "TransactionAuthorized", payload: { ... } };
},
},The framework then handles the rest in the correct order:
- Persist events first (guarantee durability)
- Apply events to rebuild state
- Publish events to the event bus (for projections)
Benefits:
- Pure functions — Handlers are decision-makers, not orchestrators
- Correct ordering — Framework controls persist-then-publish
- Easy testing — Call the function, assert the returned events
- Single responsibility — Handler decides; framework persists and publishes
- Multiple events — Return an array for multiple events:
return [event1, event2]
Trade-offs
- No mid-handler publishing — You cannot publish an event, wait for a projection to update, then publish another. All events from a single command are batched.
- No conditional async side effects — You cannot call an external service and conditionally produce more events based on its response within the same handler invocation. (Use a saga/process manager instead.)
These limitations are intentional — they enforce the Decider pattern where each command invocation is a single, atomic decision.
Example
// Handler returns single event
commands: {
CreateBankAccount: (command) => ({
name: "BankAccountCreated",
payload: { id: command.targetAggregateId },
}),
// Handler returns one of two possible events
AuthorizeTransaction: (command, state) => {
if (state.availableBalance < command.payload.amount) {
return { name: "TransactionDeclined", payload: { ... } };
}
return { name: "TransactionAuthorized", payload: { ... } };
},
},The framework persists whichever event(s) are returned, applies them to update state, and publishes them for projections.