Skip to content

Part 3: Aggregates

TIP

Further information about Aggregates can be found in the documentation. The starting point is: Documentation

Create new Aggregate "Aqua"

Let's start with the best practices and use a new domain called Aqua. First we create a corresponding folder structure for the new domain so that we can use the packages more easily later:

go
domain
└── aqua
    ├── aggregate
    ├── command
    ├── query
    ├── reactor
    └── readmodel

Then, we will create the Aqua Aggregate. The standard implementation BaseAggregate must be used as follows to define your own aggregates:

go
// domain/aqua/aggregate/agg.go
package aggregate

import "github.com/gradientzero/comby/base"

const Domain = "Aqua"

type Aqua struct {
	base.BaseAggregate

	// References
    // ... any references to other aggregates

	// Entities
    // ... any entities that are part of the aggregate

	// Value Objects
    // any value objects that are part of the aggregate
	Name    string
}

This aggregate should also be used by the facade and requires a function to create an instance.

go
// domain/aqua/aggregate/agg.go
// ...

// NewAggregate creates new aggregate object
func NewAggregate() base.Aggregate {
	agg := &Aqua{}
	agg.BaseAggregate = base.NewBaseAggregate()
	agg.Domain = Domain
	return agg
}

Finally comes event handling, i.e. the processing of the aggregate's events.

go
// domain/aqua/aggregate/agg.go
import (
	"github.com/gradientzero/comby/base"
	"github.com/gradientzero/comby/event"
)
// ...
// OnEventData applies events to the current instance of the aggregate
func (agg *Aqua) OnEventData(_evtData event.EventData) error {
    // no events yet
	return nil
}

Add Events for "Aqua"

The next step is to define the events that can be applied to the Aqua aggregate. Best practices also include managing the intent and the resulting event in a single file.

go
// domain/aqua/aggregate/add.go
package aggregate

import (
	"fmt"
	"github.com/gradientzero/comby/base"
)

type AddedEvent struct {
	AquaUuid string `json:"aquaUuid"`
	Name     string `json:"name"`
}

// intention on current aggregate
func (agg *Aqua) Add(name string) error {

	// business logic
	if len(name) < 1 {
		return fmt.Errorf("name is required")
	}

	// create new event based on the current aggregate
	evt := base.NewEventFromAggregate(agg, &AddedEvent{
		AquaUuid: agg.GetAggregateUuid(),
		Name:     name,
	})

	// apply event on the current aggregate
	return agg.ApplyEvent(agg, evt, true)
}

The intention applies the newly created event directly and changes the corresponding Aqua Aggregate instance. In order to define the change precisely, we can now enter our new event on OnEventData.

go
//...
var Events = []event.EventData{
    &AddedEvent{},
}
// ...
func (agg *Aqua) OnEventData(_evtData event.EventData) error {
	switch evtData := _evtData.(type) {
	case *AddedEvent:
		agg.Name = evtData.Name
	}
	return nil
}

The Events variable is not necessary, but it will prove to be very useful later as soon as we register the events in the Facade or in the ReadModels.

We tell the corresponding aggregate instance to take over the Name field from the EventData. This is the only change that is necessary for the Aqua Aggregate. The AddedEvent is now fully implemented.

By convention EventData names are in past tense. In this example the intention is named Add and the corresponding EventData is named AddedEvent.