Event Handler
The Readmodel interface in comby defines the contract for managing queryable representations of domain state. It combines two key responsibilities: event handling and state restoration, enabling the read model to remain up-to-date and consistent with the event-sourced domain.r validating the command and emitting events as a result of the command being executed.
In comby an EventHandler is defined as an interface as follows:
type DomainEventHandler struct {
DomainEvtPath string
DomainEvtName string
DomainEvt DomainEvt
DomainEvtHandlerFunc DomainEventHandlerFunc
}
type EventHandler interface {
Identifier
// GetDomainEventHandlers retrieves the list of all events handlers for a given domain event.
GetDomainEventHandlers(domainEvt DomainEvt) []*DomainEventHandler
// GetDomainEvents returns a list of all supported domain events.
GetDomainEvents() []DomainEvt
}The GetDomainEventHandlers method retrieves a list of event handlers (DomainEventHandler) for a specific domain event (DomainEvt). This dynamic retrieval enables the framework to process domain events by invoking the appropriate handlers, ensuring that events are managed according to their type and context.
The GetDomainEvents method returns a list of all domain events that the event handler supports. This provides a clear overview of the event types the event handler is designed to process, facilitating event routing and system introspection.
By implementing the EventHandler interface, developers can create modular and reusable event-handling components, ensuring that domain events are consistently and efficiently managed within the comby framework.
Event RequestContext (v2.15.0+)
Since v2.15.0, events carry the RequestContext of the command that triggered them. This enables traceability without breaking CQRS boundaries — readmodels and event handlers can access sender information directly from the event, without needing to query the CommandRepository.
The Event interface includes these methods for accessing the request context:
// GetReqCtx returns the RequestContext associated with this event
func (e *Event) GetReqCtx() *RequestContext
// SetReqCtx sets the RequestContext on this event
func (e *Event) SetReqCtx(reqCtx *RequestContext) errorThe ReqCtx on an event contains the sender information from the originating command:
SenderTenantUuid— the tenant that triggered the commandSenderIdentityUuid— the identity that triggered the commandSenderAccountUuid— the account that triggered the commandSenderSessionUuid— the session used to trigger the command
This is particularly useful for history events, where the sender information is extracted from the event's ReqCtx to populate HistoryEventModel.SenderTenantUuid and HistoryEventModel.SenderIdentityUuid. See Readmodel History Events for details.
func (rm *MyReadmodel) SomeEvent(ctx context.Context, evt comby.Event, domainEvt *SomeEvent) error {
// Access who triggered this event
reqCtx := evt.GetReqCtx()
if reqCtx != nil {
fmt.Printf("Event triggered by identity: %s\n", reqCtx.SenderIdentityUuid)
}
// ...
}Event Handler Orderer
As inconspicuous as this interface may look, it can have a very big impact. Typically, event handlers are registered in the facade and the facade calls them individually. However, there are situations where we as users want to gain control over the order, for example to prevent cascading effects or to force certain processes to run in the correct order. The EventHandlerOrderer interface allows us to do just that and looks like this:
// v2.3+
type EventHandlerOrderer interface {
GetOrder() int
}Once we've implemented an EventHandler—be it a Reactor, ReadModel, or similar—we can easily specify the global sort order with a receiver method called GetOrder. The smaller the number, the earlier the EventHandler's individual DomainEventHandlers will be executed.
type AnyReactor struct {
*comby.BaseReadmodel
}
type AnyReadmodel struct {
*comby.BaseReadmodel
}
func (r *AnyReactor) AnyEvent(ctx context.Context, evt comby.Event, domainEvt *aggregate.AnyCreatedEvent) error {
// (2) AnyReactor.AnyEvent will be called AFTER AnyReadmodel.AnyEvent
}
func (r *AnyReadmodel) AnyEvent(ctx context.Context, evt comby.Event, domainEvt *aggregate.AnyCreatedEvent) error {
// (1) update internal state ...
}
func (r *AnyReactor) GetOrder() int {
return 2
}
func (r *AnyReadmodel) GetOrder() int {
return 1
}This allows us to prevent cascading effects as described here: Cascading Effects
Ordering in Default
Since comby is an application framework, the default values (domains such as Account, Tenant, Identity, etc.) should normally be called earlier than the user-implemented domains. Therefore, the default domains have been assigned a negative order value. This ensures that they are executed before any user-defined domains, which typically have a default order of 0 unless explicitly specified otherwise.
These values can be adjusted either via an environment variable:
COMBY_DEFAULT_ORDER_READMODEL=-20
COMBY_DEFAULT_ORDER_REACTOR=-10or directly at comby runtime:
comby.DEFAULT_ORDER_READMODEL = -20
comby.DEFAULT_ORDER_REACTOR = -10