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:
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:
// 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.
// 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.
// 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.
// 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.
//...
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
.