Kevin's Blog

Command Query in Go Project

In Model-View-Controller applications, a common practice is to introduce service objects to encapsulate business logic and manage the interaction to external systems. While service objects are useful abstraction, further structuring them could improve the code organization and maintainability.

Command Query Separation (CQS) is a design pattern principle that defines the incoming request handler type, which is either modify the application state - commands, or retrieving data without any side effect - queries. By applying CQS to service objects, we can create clean interface. This blog post will explore further the benefits of CQS and how to create command and query objects in existing application.

Service Object’s challenges

When using service objects, the application architecture is structured as follows

services

Anything could be thrown to the service layer, which is a good convenience at first. When the project grows, the service layer also grows and become complicated to maintain. Take a look at this services diagram

services dependencies

The service layer can quickly become bloated since any type of business logic can be encapsulated within. When start working on a new feature, developers often plan to create new service object if no existing service. The more features developed, the higher number of service objects added, which makes the system more complex and difficult to maintain.

In addition, service objects can introduce dependencies on one another, creating a tangled relationship between services. This adds more difficulty to understand and modify, as changes one service may have unintended consequences to others.

An effort could be made to unify the service’s interfaces, however, this is going to be a challenging task that developers may overlook. A standardized interface is introduced early in the project, it may become technical debt that is abandoned when the requirements are evolved.

Command Query Separation

To improve maintainability, testability and code readability, the service object could be categorized either as command object, which modifies the application’s state, or query object, which returning data without side effects.

By using this definition, an interface could be introduced for all Command handlers.

type CommandHandler[C any] interface {
	Handle(ctx context.Context, cmd C) error
}

The command handler receives a C generic command object, which is just a Go struct. Each specific command handler could specifies its C parameter type, performs some actions and returns an error represents if the actions was completed.

Let’s say we are building a command to create an article, the command receives a struct contains field Title and field Body. This setup looks like below:

type CreateArticle struct {
	Title string
    Body  string
}

type CreateArticleHandler = common.CommandHandler[CreateArticle]

type createArticleHandler struct {
}

func (c createArticleHandler) Handle(ctx context.Context, cmd CreateArticle) error {
    // TODO
}

CreateArticleHandler is an interface that implements the generic CommandHandler interface, tailored in handling CreateArticle command. The code defines the CreateArticleHandler as a type follows the CommandHandler, where the generic type is replaced by CreateArticle. Later on, it creates a struct createArticleHandler in lowercase that implement the CreateArticleHandler, it means the createArticleHandler must have the Handle function that accepts a CreateArticle object as an input.

For Query handlers, the interface looks a bit different

type QueryHandler[Q any, R any] interface {
	Handle(ctx context.Context, q Q) (R, error)
}

The query handler receives a Q generic query object and a R generic result object, which are go struct. Similar to the command handler, each query handler despecifiesfines its Q and R types, performs some actions and returns the result with type R and an error represents if the process was successful.

type GetArticle struct {
	ID uuid.UUID
}

type GetArticleHandler = common.QueryHandler[GetArticle, *domain.Article]

type getArticleHandler struct {
	repo domain.Repository
}

func (c getArticleHandler) Handle(
	ctx context.Context,
	q GetArticle,
) (*domain.Article, error) {
	article, err := c.repo.GetArticle(ctx, q.ID)
	if err != nil {
		return nil, err
	}

	return article, nil
}

GetArticleHandler is an interface that implements the generic QueryHandler interface, specified in handling GetArticle query and returning the Article pointer from the domain package. It uses a private struct getArticleHandler that implements the GetArticleHandler interface, so this struct has a Handle function that accepts a GetArticle object as an input, then constructs and returns the Article pointer.

In this example, the getArticleHandler has an dependency to the Article repository. The repository handles the interaction with the data source to fetch the article. With a simple GetArticle function provided by the repository, the query doesn’t require to know how the repository get the article from, this helps to encapsulate the persistence logic from the query object.

CQS Advantages

The CQS pattern could be confused with the CQRS (Common Queue Responsibility Segregation), they both have the Command and Query concepts. The CQRS is usually implemented with an event bus, the pattern is popular in a system built with microservice architecture.

CQS emphrasizes the separation of commands and queries and could apply to any existing architecture. When introducing CQS to an application, the application is easier to test, improve the scalability and establishing a clear structure.

In term of testing, the query doesn’t create any side effect, hence the test focus on the construct the query result from its dependencies. From the above example, the GetArticleHandler only depends on the repo, the test can simulate happy path and non-happy path by assuming the result returning by the repository. Testing the command is different, since the command doesn’t return a value but an error, the testing focuses on the side effect it makes. By focusing in a single aspect, the test scenario is more comprehend to build and maintain.

The command and query implements the generic command handler and query handler interface, additional functionalities such as logging could be implemented without touching to the command/query handler. To provide a clear picture, here is a pattern I am using when defind the command handler. The query decorators could be implemented with similar approach.

baseHandler := createArticleHandler{}

func DecorateCommand[H any](handler CommandHandler[H], logger: logger) {
	return commandLogging {
		base:   handler,
		logger: logger
	}
}

type commandLogging[C any] struct { // implement CommandHandler
	base: CommandHandler[C]
	logger: zerolog.Logger
}

func (d commandLogging[C]) Handle(ctx context.Context, cmd C) (err error) {
	logger := d.logger.
		With().
		Str("command", generateActionName(cmd)).
		Str("command_body", fmt.Sprintf("%+v", cmd)).
		Logger()

	logger.Debug().Msg("Executing Command")

	defer func() {
		if err == nil {
			logger.Info().Msg("Command executed successfully")
		} else {
			logger.Error().Err(err).Msg("Failed to execute command")
		}
	}()

	return d.base.Handle(ctx, cmd)
}

The commandLogging produces the logs when running the command handler. It adds a log entry before calling the handler Handle function, and depending on the Handle function’s result, it will produce success or failure log entry. The commandLogging also implements the CommandHandler interface, the consumer doesn’t require to change.

Let’s assume the application expects the GetArticleHandler as a dependency, the application could be initialized as follow

	func NewApplication(cmdHandler GetArticleHandler) Application {...}
	cmdHandler := createArticleHandler{}

	app := NewApplication(cmdHandler)

Using the DecorateCommand function to add the logging functionality, the code is changed to

	wrappedHandler := DecorateCommand(baseHandler, zerolog.New(os.StdErr))

	app := NewApplication(cmdHandler)

The application layer remains unaffected by the wrapped GetArticleHandler. The app’s behavior remains unchanged, requiring no modifications to implement this setup.

This setup adheres to the open for extension, closed for modification principle, enabling the addition of logging functionality without altering the createArticleHandler function directly.

The DecorateCommand function applies the decorators, wrapping the original handler with a commandLogging struct. This allows for the application of multiple decorators to the original handler, each providing additional functionalities while keeping the original handler agnostic to these modifications.

Recap

CQS enhances the modularity and maintainability of the application by separating commands (which modify the system state) from queries (which retrieve information). This alignment with the Domain-Driven Design (DDD) paradigm, which favors the Command-Query Separation (CQS) pattern, makes the transition to a DDD-based architecture more seamless.

#architecture #golang #domain driven development

Reply to this post by email ↪