Concepts at a Glance
A one-screen summary of the building blocks — aggregates, projections, sagas, and how they fit together
This page is a quick reference. If you have not run the Quick Start yet, do that first — the concepts make a lot more sense once you have seen them work.
The five building blocks
Aggregate
A consistency boundary. Processes commands, enforces invariants, produces events. Defined as initialState + decide + evolve:
const BankAccount = defineAggregate<BankAccountDef>({
initialState: { balance: 0 },
decide: {
AuthorizeTransaction: (command, state, infra) => {
/* return events */
},
},
evolve: {
TransactionAuthorized: (event, state) => {
/* return new state */
},
},
});decide decides; evolve evolves state from events. Both are pure functions of their arguments. → Defining Aggregates
Command
Imperative. The thing the user is asking the aggregate to do.
type BankAccountCommand = DefineCommands<{
CreateBankAccount: void;
AuthorizeTransaction: { amount: number; merchant: string };
}>;Carries a name, a targetAggregateId, and an optional payload. → Messages & Types
Event
Past-tense fact. The thing that actually happened.
type BankAccountEvent = DefineEvents<{
BankAccountCreated: { id: string };
TransactionAuthorized: { id: string; amount: number; merchant: string };
}>;Immutable. Persisted. Replayed by evolve to rebuild state on load.
Projection
The query side of CQRS. Builds read-optimized views from event streams.
const BankAccountProjection = defineProjection<BankAccountProjectionDef>({
on: {
TransactionAuthorized: {
reduce: (event, view) => ({
...view,
balance: view.balance + event.payload.amount,
}),
},
},
queryHandlers: {
/* ... */
},
});Saga
Process manager. Listens to events, emits commands. The structural inverse of an aggregate.
const OrderFulfillmentSaga = defineSaga<OrderFulfillmentSagaDef>({
initialState: { status: null },
startedBy: ["OrderPlaced"],
on: {
OrderPlaced: {
id: (event) => event.payload.orderId,
handle: (event, state) => ({
state: { ...state, status: "awaiting_payment" },
commands: { name: "RequestPayment" /* ... */ },
}),
},
},
});→ Sagas
How they fit together
defineDomain captures the structure; wireDomain plugs in infrastructure:
const myDomain = defineDomain({
writeModel: { aggregates: { BankAccount } },
readModel: { projections: { BankAccount: BankAccountProjection } },
processModel: { sagas: { OrderFulfillment: OrderFulfillmentSaga } },
});
const domain = await wireDomain(myDomain);With no second argument you get in-memory defaults. Swap to real persistence with adapters.
What's next
- Quick Start — see all of this in one runnable file
- Core Concepts — the deeper foundations
- Why noddde — how this compares to NestJS CQRS, hand-rolled, Effect, Wolkenkit