Skip to content

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.

go
// 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.

go
// 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.

go
// 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.