Commands & CommandHandler
A CommandHandler
processes user intentions that change the state of aggregates. They are responsible for handling commands and interact with aggregates by invoking both read and write methods.
From a technical perspective, implementing a CommandHandler involves adhering to the CommandHandler
interface. This interface defines the methods GetCommandDataList
and HandleCommand
. The GetCommandDataList
method returns a list of all known CommandData
that this handler is able to process. The HandleCommand
method processes the command itself.
Here, we present the best practices for implementing a CommandHandler
that processes commands and returns a list of newly generated events. Similar to handling Queries, Events, and Aggregates, create a new package folder named command and then create the file domain/simple/command/handler.go
within that folder.
// domain/simple/command/handler.go
package command
import (
"fmt"
"github.com/gradientzero/es-cqrs-base/base"
)
type commandHandler struct {
AggregateRepository base.AggregateRepository
}
// Make sure it implements interface
var _ base.CommandHandler = (*commandHandler)(nil)
func NewCommandHandler(
AggregateRepository base.AggregateRepository,
) base.CommandHandler {
ch := &commandHandler{}
ch.AggregateRepository = AggregateRepository
return ch
}
// fullfilling base.CommandHandler interface
func (ch *commandHandler) GetCommandDataList() []base.CommandData {
return []base.CommandData{
&SimpleCommandDoSomething{},
// ... &SimpleCommandDoSomethingElse{},
}
}
func (ch *commandHandler) HandleCommand(cmd base.Command) ([]base.Event, error) {
// optional: custom service logic like authorization
// ...
// handle command
if cmd.GetData() != nil {
return cmd.GetData().HandleCommandData(ch, cmd)
} else {
return nil, fmt.Errorf("could not handle command, unknown type %s", cmd.GetDataType())
}
}
Unlike EventHandlers, Commands are processed by a single CommandHandler
that has added the corresponding CommandData
(in this case, SimpleCommandDoSomething
) to the list. Within the HandleCommand
method, operations applicable to all commands can be performed. Subsequently, HandleCommandData
method of the underlying CommandData
of the provided Command
is called.
By convention, each CommandData
is written in its own file. In this example, the CommandData SimpleCommandDoSomething
will be stored in domain/simple/command/do_something.go
. This CommandData is designed to create a new aggregate.
// domain/simple/command/do_something.go
package command
import (
"fmt"
"my/domain/simple/aggregate"
"github.com/gradientzero/es-cqrs-base/base"
)
// SimpleCommandDoSomething is the CommandData
type SimpleCommandDoSomething struct {
NewAggregateUuid string `json:"newAggregateUuid"`
Name string `json:"name,omitempty"`
}
// Make sure it implements interface
var _ base.CommandData = (*SimpleCommandDoSomething)(nil)
// HandleCommandData handles underlying command
func (cmdData *SimpleCommandDoSomething) HandleCommandData(_cmdHandler base.CommandHandler, cmd base.Command) ([]base.Event, error) {
// cast into a concrete type
cmdHandler, ok := _cmdHandler.(*commandHandler)
if !ok {
return nil, fmt.Errorf("%s failed - could not cast abstract aggregate to concrete type", base.GetTypeName(cmdData))
}
// retrieve existing aggregate from store
_agg, _ := cmdHandler.AggregateRepository.GetAggregate(cmdData.NewAggregateUuid, aggregate.NewAggregate)
if _agg != nil {
return nil, fmt.Errorf("%s failed - aggregateUuid already exist", base.GetTypeName(cmdData))
}
// create new aggregate
_agg = aggregate.NewAggregate()
// cast into a concrete type
agg, ok := _agg.(*aggregate.SimpleAggregate)
if !ok {
return nil, fmt.Errorf("%s failed - could not cast abstract aggregate to concrete type", base.GetTypeName(cmdData))
}
agg.AggregateUuid = cmdData.NewAggregateUuid
// execute logic
err := agg.DoSomething(cmdData.Name)
if err != nil {
return nil, err
}
// return new events
return agg.GetUncommittedEvents(), nil
}
First, we define our CommandData
(SimpleCommandDoSomething
) and implement the HandleCommandData
method, as required by the base.CommandData
interface. Then, we cast the CommandHandler to our specific CommandHandler
so that we can access the AggregateRepository
. Next, we simply check if there is already an aggregate with the provided AggregateUuid
. If not, we create a new aggregate and cast it to our specific *aggregate.SimpleAggregate
type. Finally, we invoke the DoSomething
intention on the aggregate and return the list of newly generated events.
Similarly, other CommandData
structs follow a similar pattern. Here's an example of a CommandData
that doesn't create a new aggregate but instead interacts with an existing aggregate.
// domain/simple/command/do_something_else.go
package command
import (
"fmt"
"my/domain/simple/aggregate"
"github.com/gradientzero/es-cqrs-base/base"
)
// SimpleCommandDoSomethingElse is the CommandData
type SimpleCommandDoSomethingElse struct {
AggregateUuid string `json:"aggregateUuid"`
Name string `json:"name,omitempty"`
}
// Make sure it implements interface
var _ base.CommandData = (*SimpleCommandDoSomethingElse)(nil)
// HandleCommandData handles underlying command
func (cmdData *SimpleCommandDoSomethingElse) HandleCommandData(_cmdHandler base.CommandHandler, cmd base.Command) ([]base.Event, error) {
// cast into a concrete type
cmdHandler, ok := _cmdHandler.(*commandHandler)
if !ok {
return nil, fmt.Errorf("%s failed - could not cast abstract aggregate to concrete type", base.GetTypeName(cmdData))
}
// retrieve existing aggregate from store
_agg, _ := cmdHandler.AggregateRepository.GetAggregate(cmdData.AggregateUuid, aggregate.NewAggregate)
if _agg == nil {
return nil, fmt.Errorf("%s failed - aggregate does not exist", base.GetTypeName(cmdData))
}
// cast into a concrete type
agg, ok := _agg.(*aggregate.SimpleAggregate)
if !ok {
return nil, fmt.Errorf("%s failed - could not cast abstract aggregate to concrete type", base.GetTypeName(cmdData))
}
// execute logic
err := agg.DoSomethingElse(cmdData.Name)
if err != nil {
return nil, err
}
// return new events
return agg.GetUncommittedEvents(), nil
}
It's worth noting that through the AggregateRepository
, we have the capability to access other aggregates from different domains. This allows us to load multiple foreign aggregates and use them within our command. For instance, if we want to verify the existence of a specific referenced aggregate
, we can retrieve it first before modifying our own aggregate.