Command Handler
The CommandHandler
interface in comby defines the contract for handling domain-specific commands within the framework. It embedds the Identifier
interface, ensuring that every command handler is uniquely associated with a Domain and Name, and providing additional methods for managing command execution.
In comby an CommandHandler
is defined as an interface
as follows:
type DomainCommandHandler struct {
DomainCmdPath string // Technical path of the domain command
DomainCmdName string // Name of the domain command
DomainCmd DomainCmd // Runtime instance of the domain command
DomainCmdHandlerFunc DomainCommandHandlerFunc // Function to handle the domain command
}
type CommandHandler interface {
Identifier
// GetDomainCommandHandler retrieves the handler for a given domain command.
GetDomainCommandHandler(domainCmd DomainCmd) *DomainCommandHandler
// GetDomainCommands returns a list of all supported domain commands.
GetDomainCommands() []DomainCmd
}
The primary purpose of the CommandHandler
is to map domain commands to their corresponding handler. This is achieved through the GetDomainCommandHandler method, which retrieves the appropriate function for processing a specific DomainCmd
. By using this method, the framework dynamically invokes the correct logic based on the type of the domain command, ensuring flexibility and scalability.
The GetDomainCommands method returns a list of all domain commands supported by the handler. This list enables the framework to identify the commands that a handler can process, ensuring that commands are routed to the appropriate handler based on their domain and type.
By implementing the CommandHandler
interface, developers can define modular and reusable command-handling logic that seamlessly integrates with the event-driven architecture of comby.
Base Command Handler
The BaseCommandHandler
is a foundational implementation of the CommandHandler
interface in comby, designed to manage domain commands and their associated handler functions. It provides a structured and reusable base for building command-handling logic in event-sourced systems. The structure of BaseCommandHandler
is defined as follows:
type BaseCommandHandler struct {
*BaseIdentifier // Embeds base identifier
domainCmdMap map[string]*DomainCommandHandler // Registry of domain command handlers
}
At its core, the BaseCommandHandler
embeds a BaseIdentifier
, ensuring that each handler is uniquely identified by its Domain and Name. The handler maintains a registry (domainCmdMap) that maps domain commands to their metadata and handling logic. Each entry in this registry is represented by the DomainCommandHandler
struct, which includes details such as the domain command's type path, name, runtime instance, and handler function.
The BaseCommandHandler
can be initialized using the NewBaseCommandHandler
function. This function assigns a default domain and generates a unique name for the handler instance, ensuring proper identification within the system.
Key methods include GetDomainCommandHandler, which retrieves the handler function for a given domain command by matching its type path. If no handler is found, the method returns nil. Another method, GetDomainCommands, provides a list of all registered domain commands that the handler is capable of processing, making it easy to query the handler’s capabilities.
The internal addDomainCommandHandler method is used to register new domain command handlers. It accepts a domain command and its corresponding handler function, ensuring that both are valid and that no duplicate handlers exist for the same command type. If successful, it adds the handler to the registry, making it available for command processing.
In comby, domain command handlers are registered using the comby.AddDomainCommandHandler
method. This approach ensures that domain event handlers are correctly linked to their respective aggregate and can be invoked during event processing.
User-Defined Command Handlers
User-defined command handlers in comby are custom implementations of command processing logic tailored to a specific domain. These handlers extend the functionality of the BaseCommandHandler
by integrating domain-specific logic and registering domain command handlers for managing operations on aggregates. A user-defined command handler typically includes the following components:
type cmdHandler struct {
*comby.BaseCommandHandler
AggregateRepository *comby.AggregateRepository[*aggregate.Order]
}
func NewCommandHandler(
AggregateRepository *comby.AggregateRepository[*aggregate.Order],
) *cmdHandler {
ch := &cmdHandler{}
ch.BaseCommandHandler = comby.NewBaseCommandHandler()
ch.Domain = "Order"
ch.AggregateRepository = AggregateRepository
// register domain comannd handlers
comby.AddDomainCommandHandler(ch, ch.PlaceOrder)
comby.AddDomainCommandHandler(ch, ch.MarkOrderPaid)
// ...
return ch
}
In the provided example, the cmdHandler
struct defines a command handler for the "Order" domain. It embeds the BaseCommandHandler
to inherit its core functionality, including identifier metadata and domain command management. Additionally, it includes an AggregateRepository
for managing instances of the Order aggregate, enabling interaction with the aggregate’s state and ensuring seamless event sourcing.
The NewCommandHandler
function initializes the cmdHandler
. It sets up the base command handler, assigns the "Order" domain to it, and binds the provided AggregateRepository
for accessing and modifying aggregate data. During initialization, domain-specific command handlers are registered using comby.AddDomainCommandHandler
, linking command types to their corresponding handler methods in the cmdHandler.
The example demonstrates the registration of two command handlers, including: PlaceOrder
and MarkOrderPaid
. These handlers encapsulate the logic for processing specific domain commands, such as creating new orders or marking existing orders as paid. By registering these handlers, the cmdHandler can effectively manage the lifecycle and state transitions of Order aggregates.
Add Domain Command Handler: PlaceOrder
Any domain command handler must fullfill following signature:
type TypedDomainCommandHandlerFunc[T DomainCmd] func(ctx context.Context, cmd Command, domainCmd T) ([]Event, error)
The PlaceOrder
domain command handler manages the process of placing a new order in the "Order" domain. It validates the command, checks for pre-existing aggregates, creates a new aggregate if necessary, and executes the business logic for placing the order. Finally, it returns the uncommitted events generated during the process.
type PlaceOrderItem struct {
Sku string `json:"sku"`
Quantity int64 `json:"quantity"`
}
type PlaceOrder struct {
OrderUuid string `json:"orderUuid"`
Item *PlaceOrderItem `json:"item"`
}
func (ch *cmdHandler) PlaceOrder(ctx context.Context, cmd comby.Command, domainCmd *PlaceOrder) ([]comby.Event, error) {
// validate uuid
if err := utils.ValidateUuid(domainCmd.OrderUuid); err != nil {
return nil, fmt.Errorf("%s failed - orderUuid is invalid", utils.GetTypeName(domainCmd))
}
// retrieve existing aggregate from store
if _agg, _ := ch.AggregateRepository.GetAggregate(ctx, domainCmd.OrderUuid); _agg != nil {
return nil, fmt.Errorf("%s failed - order already exist", utils.GetTypeName(domainCmd))
}
// create new aggregate
agg := aggregate.NewAggregate()
agg.AggregateUuid = domainCmd.OrderUuid
// execute logic
if err := agg.Place(&aggregate.Item{
Sku: domainCmd.Item.Sku,
Quantity: domainCmd.Item.Quantity,
}); err != nil {
return nil, err
}
// return new events
return agg.GetUncommittedEvents(), nil
}
The PlaceOrder
handler is defined as a receiver method on the cmdHandler
struct. It takes three parameters: a context.Context for managing the execution context, a comby.Command containing the metadata, and a PlaceOrder
domain command with the specifics of the order to be placed.
The process begins by validating the OrderUuid from the PlaceOrder
command using utils.ValidateUuid. If the UUID is invalid, the handler returns an error indicating the issue. Next, the handler checks if an aggregate with the specified OrderUuid already exists in the AggregateRepository
. If an aggregate is found, it returns an error, as placing an order for an existing UUID would violate the uniqueness constraint.
If no existing aggregate is found, the handler creates a new Order aggregate using the aggregate.NewAggregate
method and assigns the OrderUuid from the command. It then executes the aggregate's Place
method, passing the item details from the command. This step applies the domain logic for placing an order, including any validations or state updates required by the aggregate.
After successfully executing the logic, the handler retrieves the aggregate's uncommitted events using agg.GetUncommittedEvents
and returns them. These events represent the state changes resulting from placing the order and will be persisted by the framework's EventStore
.