Skip to content

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:

go
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:

go
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:

go
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:

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

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