Aggregates
The standard implementation BaseAggregate
must be used as follows to define your own aggregates:
package aggregate
import (
"github.com/gradientzero/es-cqrs-base/base"
)
type SimpleAggregate 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
MyName string
}
// Make sure it fullfills interface
var _ base.Aggregate = (*SimpleAggregate)(nil)
// NewAggregate creates new aggregate object
func NewAggregate() base.Aggregate {
agg := &SimpleAggregate{}
agg.BaseAggregate = base.NewBaseAggregate()
agg.AggregateType = "SimpleAggregate"
return agg
}
The provided BaseAggregate
struct has been embedded into a user defined SimpleAggregate
struct. But, make a user defined aggregate implementation usable by the Facade
, a constructor method must be provided. By convention this method is called NewAggregate
, which creates a new SimpleAggregate
aggregate and returns it as an interface Aggregate
. Further, a unique aggregate name must be defined in AggregateType
to differentiate between aggregates.
Intentions / Actions
An Aggregate holds the responsibility of processing user intentions and generating corresponding events. User intentions are implemented as receiver methods
within the Aggregate. When executing an intention, the current state of the aggregate should be checked against the intention. If the intention is to update a specific value object, but there is no difference with the current aggregate's state, it does not make sense to generate a new Event
. However, if there are differences, a new Event
is generated using the helper function base.NewEventFromAggregate
. This function takes the aggregate
instance and the intention's EventData
to create a new Event
containing the corresponding EventData
for this particular aggregate in its current state.
Next, the intention applies the newly generated Event
to the aggregate using another helper function base.ApplyEvent
. This function takes care of all necessary steps for Event Sourcing, including versioning and tracking new events to be committed in the EventStore. Moreover, base.ApplyEvent
automatically executes the underlying ApplyEvent
of the embedded BaseAggregate
and ApplyEventData
for the corresponding EventData
, as implemented by the user. Since the Facade
works with the Aggregate
interface, the user needs to cast the interface to their specific aggregate to access the concrete aggregate's implementation.
By convention, the intent, the definition of EventData
and the corresponding ApplyEventData
method are implemented in the same file. This design promotes clarity and consistency, making it easier for developers to comprehend and maintain the codebase. Here is one example intention DoSomething
for SimpleAggregate
:
package aggregate
import (
"fmt"
"github.com/gradientzero/es-cqrs-base/base"
)
// DidSomethingEvent (EventData for intention DoSomething)
type DidSomethingEvent struct {
AggregateUuid string `json:"aggregateUuid"`
Name string `json:"name,omitempty"`
}
// Make sure it fullfills interface
var _ base.EventData = (*DidSomethingEvent)(nil)
// DoSomething intention on current aggregate
func (agg *SimpleAggregate) DoSomething(name string) error {
// ... any logic like checking name is valid
if agg.MyName == name {
return fmt.Errorf("name already set to %s", name)
}
// create event and apply to aggregate
evt := base.NewEventFromAggregate(agg, &DidSomethingEvent{
AggregateUuid: agg.GetAggregateUuid(),
Name: name,
})
// apply event
return base.ApplyEvent(agg, evt, true)
}
// fullfilling base.EventData interface
func (evtData *DidSomethingEvent) ApplyEventData(_agg base.Aggregate) error {
// cast into a concrete type
agg, ok := _agg.(*SimpleAggregate)
if !ok {
return fmt.Errorf("%s failed - could not cast abstract aggregate to concrete type", base.GetTypeName(evtData))
}
// apply change
agg.MyName = evtData.Name
return nil
}
The new Event
was added to the aggregate and will be persisted by the Facade
in the EventStore
later. By convention EventData
names are in past tense. In this example the intention is named DoSomething
and the corresponding EventData is named DidSomethingEvent
.
In addition, based on best practices, it is recommended to implement the aggregate
and its intentions in a separate sub-package called aggregate
, which lives within a domain-specific package. For example, the SimpleAggregate
could be located in domain/simple/aggregate/aggregate.go
. The intention DoSomething
and the corresponding EventData
could be located in domain/simple/aggregate/do_something.go
.
Testing aggregates
As a user, you create a new aggregate and call up the corresponding intentions. A simple example can be seen here:
package aggregate_test
import (
"fmt"
"my/app/domain/simple/aggregate"
"testing"
"github.com/gradientzero/es-cqrs-base/base"
)
func TestSimpleAggregate(t *testing.T) {
// create new SimpleAggregate and return instance of type base.Aggregate
_agg := aggregate.NewAggregate()
// cast into a concrete type
agg, ok := _agg.(*aggregate.SimpleAggregate)
if !ok {
t.Error("could not cast abstract aggregate to concrete type")
}
if agg.GetAggregateType() != "SimpleAggregate" {
t.Errorf("wrong aggregate value: %s", agg.GetAggregateType())
}
if agg.GetAggregateUuid() == "" {
t.Errorf("wrong aggregate value: %s", agg.GetAggregateUuid())
}
if agg.GetVersion() != 1 {
t.Errorf("wrong aggregate value: %d", agg.GetVersion())
}
if len(agg.GetUncommittedEvents()) != 0 {
t.Errorf("wrong aggregate value: %d", len(agg.GetUncommittedEvents()))
}
// execute logic
NewName := "NewName001"
if err := agg.DoSomething(NewName); err != nil {
t.Error(err)
}
if err := agg.DoSomething(NewName); err == nil {
t.Fatalf("should have raised an error")
}
// checks
if agg.GetVersion() != 2 {
t.Errorf("wrong aggregate value: %d", agg.GetVersion())
}
if agg.MyName != NewName {
t.Errorf(fmt.Sprintf("wrong aggregate value: '%s'", agg.MyName))
}
evts := agg.GetUncommittedEvents()
if len(evts) == 0 {
t.Fatalf("no events generated")
}
evt := evts[0]
if evt.GetDataType() != base.GetTypeName(aggregate.DidSomethingEvent{}) {
t.Error(evt)
}
}
Define Entities
Entities exist within Aggregates only.
type MyEntity struct {
AnyField string
// ... any other fields
}
type SimpleAggregate struct {
base.BaseAggregate
// References
// ... any references to other aggregates
// Entities
MyEntity *MyEntity
OrAsList []*MyEntity
// Value Objects
}
Use References
References to Aggregates using only strings (uuid).
// domain/foo/aggregate/aggregate.go
package aggregate
import (
"fmt"
"github.com/gradientzero/es-cqrs-base/base"
)
type FooAggregate struct {
base.BaseAggregate
// ...
}
// domain/bar/aggregate/aggregate.go
package aggregate
import (
"fmt"
"github.com/gradientzero/es-cqrs-base/base"
)
type BarAggregate struct {
base.BaseAggregate
// ...
// References
FooUuid string
}
BarAggregate
has no knowledge of the content of FooAggregate
. It only knows the UUID
. If the FooAggregate is deleted, BarAggregate should become aware of this through an EventHandler
and set its reference to null accordingly.