Skip to content

Queries & QueryHandler

QueryHandlers serve the purpose of answering user queries without altering the state of aggregates. They are inherently read-only and can only invoke read-only methods.

From a technical standpoint, implementing QueryHandlers involves adhering to the QueryHandler interface. This interface defines the methods GetQueryDataList and HandleQuery. The GetQueryDataList returns a list of all known QueryData this handler is able to process. The HandleQuery method processes the query itself.

Here, we present the best practices for implementing a QueryHandler that handles requests and returns a response of type Response. Similar to handling Commands, Events, and Aggregates, create a new package folder named query and then create the file domain/simple/query/handler.go within that folder.

go
// domain/simple/query/handler.go
package query

import (
	"fmt"
	"my/app/domain/simple/readmodel"
	"github.com/gradientzero/es-cqrs-base/base"
)

type queryHandler struct {
	SimpleReadmodel        readmodel.ReadModel
}

// Make sure it implements interface
var _ base.QueryHandler = (*queryHandler)(nil)

func NewQueryHandler(
	SimpleReadmodel readmodel.ReadModel,
) base.QueryHandler {
	qh := &queryHandler{}
	qh.SimpleReadmodel = SimpleReadmodel
	return qh
}

// fullfilling base.QueryHandler interface
func (qh *queryHandler) GetQueryDataList() []base.QueryData {
	return []base.QueryData{
		&SimpleQueryModel{},
		// ... &SimpleQueryList{},
	}
}

func (qh *queryHandler) HandleQuery(qry base.Query) (*base.Response, error) {
	// optional: custom logic like authorization
	// ...
	if qry.GetData() != nil {
		return qry.GetData().HandleQueryData(qh, qry)
	} else {
		return nil, fmt.Errorf("could not handle query, unknown type %s", qry.GetDataType())
	}
}

Unlike EventHandlers, Queries are processed by a single QueryHandler that has added the corresponding QueryData (in this case, SimpleQueryModel) to the list. Within the HandleQuery method, operations applicable to all queries can be performed. Subsequently, HandleQueryData method of the underlying QueryData of the provided Query is called.

By convention, each QueryData is written in its own file. In this example, the QueryData SimpleQueryModel will be stored in domain/simple/query/qry.model.go. This QueryData is designed to simply return the internal model of a specific aggregate.

go
// domain/simple/query/qry.model.go
package query

import (
	"fmt"
	"github.com/gradientzero/es-cqrs-base/base"
)

// SimpleQueryModel is the QueryData
type SimpleQueryModel struct {
	AggregateUuid   string `json:"aggregateUuid,omitempty"`
	SomeOtherParams string `json:"someOtherParams,omitempty"`
}

// Make sure it implements interface
var _ base.QueryData = (*SimpleQueryModel)(nil)

// fullfilling base.QueryData interface
func (qryData *SimpleQueryModel) HandleQueryData(_qryHandler base.QueryHandler, qry base.Query) (*base.Response, error) {
	// retrieve query handler
	qryHandler, ok := _qryHandler.(*queryHandler)
	if !ok {
		return nil, fmt.Errorf("%s failed - could not cast abstract aggregate to concrete type", base.GetTypeName(_qryHandler))
	}

	// retrieve item read model
	model, err := qryHandler.SimpleReadModel.GetModelByAggregateUuid(qryData.AggregateUuid)
	if err != nil {
		return nil, err
	}

	// generate response
	response := &base.Response{}
	response.Item = model

	// return response
	return response, nil
}

First, we define our QueryData (SimpleQueryModel) and implement the HandleQueryData method, as required by the base.QueryData interface. Then, we cast the QueryHandler to our specific QueryHandler so that we can access the ReadModel. Next, we simply query the ReadModel for this aggregate, and the ReadModel provides the internal model of the aggregate. This model is then written into the Response object and returned.

If we want to return a list, we should use Items instead of Item and write the list into response.Items.