Aggregate Root Modeling
Patterns
INFO
Wow framework supports both Kotlin and Java for aggregate modeling. The Bank Transfer example demonstrates a complete Java-based aggregate. Kotlin features such as default parameter values and companion objects are not available in Java, but all core annotations (@OnCommand, @OnSourcing, @AggregateRoot, etc.) work identically in both languages.
Aggregate Pattern (Recommended)
The aggregate pattern places command functions and sourcing functions (containing aggregate state data) in separate classes. This approach avoids the problem of command functions directly modifying aggregate state data (by setting the setter accessor to private). At the same time, separation of responsibilities allows the aggregate root's command functions to focus more on command processing, while sourcing functions focus on aggregate state data changes.
Simple Aggregation Pattern
Complex Aggregation Pattern
Single Class Pattern
The single class pattern places command functions, sourcing functions, and aggregate state data together in one class. The advantage is simplicity and directness.
Violation of Event Sourcing Principles
In the single class pattern, command functions can directly modify aggregate state data, which leads to:
- State changes cannot be traced via events
- Destroys the core value of Event Sourcing
- May result in inconsistent state changes
Strongly Recommended: Use this pattern only for simple scenarios or prototype development.
Inheritance Pattern
The inheritance pattern uses the state aggregate root as the base class and sets the setter accessor to private to prevent command aggregate roots from modifying aggregate state data in command functions.
Conventions
Command Aggregate Root
The command aggregate root is responsible for defining command handler functions, processing commands to execute corresponding business logic, and finally returning domain events.
- The command aggregate root needs to add the
@AggregateRootannotation so that thewow-compilermodule can generate corresponding metadata definitions. - The
@OnCommandannotation for command handler functions is not required. By default, naming a command handler functiononCommandindicates it is a command handler function.
Disabling Route Generation
Use @AggregateRoute(enabled = false) to prevent automatic command route registration for an aggregate:
@AggregateRoot
@AggregateRoute(enabled = false)
class InternalAggregate(val id: String) {
// This aggregate won't have REST API endpoints
}- The first parameter of command handler functions can be defined as: specific command (
AddCartItem), command message (CommandMessage<AddCartItem>), command message exchange (CommandExchange<AddCartItem>). - The remaining parameters of command handler functions will be obtained from the
IOCcontainer. If you have injected an instance in theSpring IOCcontainer, you can obtain it directly through parameters. - The return value of command handler functions is one or more domain events. These domain events will first have their state changed to the latest state by the state aggregate root through sourcing functions, then be persisted to the
EventStore.- When the return value type is not clear, it should be specified via
@OnCommand.returns. Otherwisewow-compilerwill not be able to identify the returned domain event type.
- When the return value type is not clear, it should be specified via
- After persistence is complete, they will be published to the event bus through the
DomainEventBus.
@AggregateRoot
class Cart(private val state: CartState) {
@OnCommand(returns = [CartItemAdded::class, CartQuantityChanged::class])
fun onCommand(command: AddCartItem): Any {
require(state.items.size < MAX_CART_ITEM_SIZE) {
"Shopping cart can only add up to [$MAX_CART_ITEM_SIZE] items."
}
state.items.firstOrNull {
it.productId == command.productId
}?.let {
return CartQuantityChanged(
changed = it.copy(quantity = it.quantity + command.quantity),
)
}
val added = CartItem(
productId = command.productId,
quantity = command.quantity,
)
return CartItemAdded(
added = added,
)
}
}AfterCommand Hook
The @AfterCommand annotation defines a post-processing hook that executes after the main command handler completes. If the method returns a non-null value, it is appended as an additional domain event to the event stream.
class OrderAggregate(val id: String) {
@OnCommand
fun onCreateOrder(command: CreateOrder): OrderCreated {
return OrderCreated(...)
}
@AfterCommand
fun afterCreateOrder(exchange: ServerCommandExchange<*>): OrderConfirmed? {
val result = exchange.getCommandInvokeResult<OrderCreated>()
// Perform post-processing, optionally return additional events
return null
}
}You can filter which commands trigger the hook using include and exclude:
@AfterCommand(include = [CreateOrder::class], exclude = [CancelOrder::class])
fun onAfterCommand(exchange: ServerCommandExchange<*>): AdditionalEvent? {
return null
}Multiple @AfterCommand functions are supported, with execution order controlled by @Order.
Error Handling with OnError
The @OnError annotation defines an error handler that executes when command processing fails:
@OnError
fun onError(command: CreateOrder, error: Throwable) {
// Handle the error, e.g., log or publish error event
}State Aggregate Root
The state aggregate root defines aggregate state data and sourcing functions.
- The state aggregate root must define the aggregate root ID field in the constructor.
- The role of sourcing functions is to apply domain events to aggregate state data, thereby changing aggregate state data.
- Sourcing functions are marked with the
@OnSourcingannotation. However, this annotation is optional. By default, when the function name isonSourcing, it indicates that the function is a sourcing function. - Sourcing functions accept parameters of: specific domain events (
CartItemAdded), domain events (DomainEvent<CartItemAdded>). - No return value needs to be defined for sourcing functions.
class CartState(val id: String) {
var items: List<CartItem> = listOf()
private set
@OnSourcing
fun onCartItemAdded(cartItemAdded: CartItemAdded) {
items = items + cartItemAdded.added
}
}