Skip to content

Aggregates

The standard implementation BaseAggregate must be used as follows to define your own aggregates:

go
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:

go
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:

go
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.

go
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).

go
// domain/foo/aggregate/aggregate.go
package aggregate

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

type FooAggregate struct {
	base.BaseAggregate
	// ...
}
go
// 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.