Skip to content

Projection

In an application framework that follows the Event Sourcing (EV) and Command Query Responsibility Segregation (CQRS) patterns, there is a strict separation between the write and read sides. The write side handles commands and business logic, ensuring that changes are applied to the system's state, while the read side focuses on serving data, often in the form of projections or read models. This separation allows for greater flexibility and scalability, as each side can be optimized independently.

Projections, which are objects fullfilling the EventHandler interface, are built by applying domain events to form a materialized view of the system’s state. Using Golang's generics and interfaces, it becomes easier to define these projections by reusing code to transform domain aggregates into read models. This flexibility allows developers to efficiently create tailored read models while maintaining the strict separation of concerns within the CQRS architecture.

Projections, or Readmodels, are typically used to serve data in response to Queries in a CQRS-based architecture. The read side focuses on efficient data retrieval. Projections are built by processing these events to form a queryable representation of the system’s state. When a Query is executed, it typically interacts with one or more projections to return the required data in a fast and efficient manner.

Simple Creation of Projections from Aggregates

In many cases, existing aggregates can be directly used to build projections without impacting the aggregates themselves or the write side of the system. This allows for a straightforward way to represent the current state of the domain in a readmodel, leveraging the data that is already available in the system.

By using this approach, you can create efficient projections while maintaining the strict separation between the read and write sides. In the next section, we will explore how custom projections can be built, offering more flexibility to tailor readmodels to specific query requirements.

// TODO: CODE EXAMPLES

Assumptions on MyAggregate
go
// domain/my/aggregate/aggregate.go

//...
const Domain = "MyAggregate"

var Events = []event.EventData{
	&MyAggregateHappendEvent{},
	// ...
}

func NewAggregate() base.Aggregate {
	agg := &MyAggregate{}
	agg.BaseAggregate = base.NewBaseAggregate()
	agg.Domain = Domain
	return agg
}
//...

Small helper conventions to simplify working with domains:

  • NewAggregate - a package function used in repositories to create typed instances of aggregates.
  • Events - a list of EventData used by both the write and read sides. Although the read side can process events unrelated to aggregates, this helper list ensures that at least the EventData from both sides remains synchronized.
go
// domain/my/domain.go
package domain

import (
	"my-module-name/domain/my/aggregate"

	"github.com/gradientzero/comby/base"
	"github.com/gradientzero/comby/projection"
)

// for example within - conventional - `Register` func:
//...
rm := projection.NewProjectionAggregate[*aggregate.MyAggregate](
		aggregate.NewAggregate,
		fc.GetEventRepository(),
		aggregate.Events,
)
// add to facade
fc.RegisterEventHandler(aggregate.Domain, rm)
fc.RegisterEventHandlerFunc(aggregate.Domain, aggregate.Events, rm.OnHandleEvent)

// next use projection somewhere (ignore error handling for now)
key := "MyKey"
val, _ := rm.GetModel(key)

Building Custom Projections for Specific Queries

go
// domain/my/domain.go
package domain

import (
	"my-module-name/domain/my/aggregate"

	"github.com/gradientzero/comby/base"
	"github.com/gradientzero/comby/projection"
)

// for example within - conventional - `Register` func:
//...
rm := projection.NewProjectionAggregate[*aggregate.MyAggregate](
		aggregate.NewAggregate,
		fc.GetEventRepository(),
		aggregate.Events,
)