id | title | description |
---|---|---|
write-side |
Write Side |
An application's write side handles commands, validates input data, and emits events based on valid commands. |
Commands are executed by objects that encapsulate domain logic. These objects are called Domain Objects. Domain Objects are grouped into Aggregates. In a CQRS/ES app, an aggregate is a transaction boundary. This means that any given aggregate should be able to execute its commands without communicating with other aggregates.
Since the write side is used only to perform commands, your aggregate can be compact, and only keep the state required for command execution.
See Martin Fowler's definition for aggregates in the DDD paradigm: https://martinfowler.com/bliki/DDD_Aggregate.html.
In reSolve, an aggregate is a static object that contains a set of functions of the following two kinds:
- Projections - build aggregate state base from events.
- Command Handlers - execute commands.
Aggregate state is explicitly passed to all of these functions as an argument.
Each aggregate instance should have a unique immutable ID. You should generate an aggregate ID on the client and send it to reSolve with a command that creates a new aggregate:
import { useCommand } from '@resolve-js/react-hooks'
...
const createShoppingListCommand = useCommand(
{
type: 'createShoppingList',
aggregateId: uuid(),
aggregateName: 'ShoppingList',
payload: {
name: shoppingListName
},
},
(err, result) => {
...
}
)
An Aggregate ID should stay unique across all aggregates in the given event store. You can use UUID v4 or cuid to generate aggregate IDs for scalable applications.
To configure aggregates in a reSolve app, specify an aggregates array in the application configuration file:
aggregates: [
{
name: 'ShoppingList',
commands: 'common/aggregates/shopping_list.commands.js',
projection: 'common/aggregates/shopping_list.projection.js'
}
]
You can emit aggregate commands in the following cases:
The reSolve framework exposes an HTTP API that you can use to send commands from the client side. Your application's frontend can use this API directly or through one of the available client libraries.
The code sample below demonstrates how to use the @resolve-js/client library to send a command:
client.command(
{
aggregateName: 'Chat',
type: 'postMessage',
aggregateId: userName,
payload: message,
},
(err) => {
if (err) {
console.warn(`Error while sending command: ${err}`)
}
}
)
You can use the resolve.executeCommand function to emit a command on the server side from a Saga or API Handler:
await resolve.executeCommand({
type: userWithSameEmail ? 'rejectUserCreation' : 'confirmUserCreation',
aggregateName: 'user',
payload: { createdUser },
aggregateId,
})
Aggregate command handlers are grouped into a static object. A command handler receives a command and a state object built by the aggregate Projection. The command handler should return an event object that is then saved to the event store. A returned object should specify an event type and a payload specific to this event type. A command handler can also validate command data and throw an error if the validation fails.
A typical Commands object structure:
export default {
// A command handler
createStory: (state, command) => {
const { title, link, text } = command.payload
// The validation logic
if (!text) {
throw new Error('The "text" field is required')
}
// The resulting event object
return {
type: 'StoryCreated',
payload: { title, text, link, userId, userName },
}
},
// ...
}
Projection functions are used to calculate an aggregate state based on the aggregate's events. A projection function receives the previous state and an event to be applied. It should return a new state based on the input.
Projection functions run for all events with the current aggregate ID. The resulting state is then passed to the corresponding command handler.
In addition to projection functions, a projection object should define an Init function. This function returns the initial state of the aggregate.
A typical projection object structure is shown below:
export default {
Init: () => ({}),
StoryCreated: (state, { timestamp, payload: { userId } }) => ({
...state,
createdAt: timestamp,
createdBy: userId,
voted: [],
comments: {}
})
...
}
All events returned by command handlers are saved to the event store. The reSolve framework uses one of the supported storage adapters to write events to the storage.
You can specify the storage adapter in the storageAdapter config section:
storageAdapter: {
module: '@resolve-js/eventstore-lite',
options: {
databaseFile: '../data/event-store.db'
}
}
Adapters for the following storage types are available out of the box:
You can also add your own storage adapter to store events. Refer to the Adapters section of the reSolve documentation for more information about adapters.