Skip to content

Reactor

A Reactor in comby is an augmented type of Readmodel designed to actively react to domain events and trigger side effects beyond state updates. Unlike traditional readmodels that primarily focus on maintaining queryable projections of domain data, a Reactor listens for domain events and takes specific actions in response.

Key Responsibilities of a Reactor:

  • Event Reaction: Reactors subscribe to domain events, processing them through registered event handlers. Each handler encapsulates the logic for reacting to a specific event. For example, a Reactor might handle an OrderPlacedEvent to send a confirmation email to the customer or notify downstream systems.

  • Command Generation: Reactors can create and dispatch new commands as a reaction to domain events. This enables the Reactor to initiate additional workflows or trigger further changes in the domain. For instance, after processing an OrderPaidEvent, a Reactor might issue a ShipOrderCommand to move the order to the next stage.

  • Side Effects: Beyond command generation, Reactors can be used for initiating side effects such as:

    • Sending emails or SMS notifications.
    • Logging or tracking events in external systems.
    • Triggering third-party integrations.

Implementation Details

A Reactor builds upon the BaseReadmodel, inheriting its event-handling and state-restoration capabilities. Event handlers in the Reactor are registered in the same way as in a readmodel, using comby.AddDomainEventHandler. However, these handlers are specifically designed to perform actions or generate new domain interactions rather than just updating internal state.

go
type Reactor struct {
	*comby.BaseReadmodel
	// ...
}

Dependencies such as the Facade or AggregateRepository can be injected during instantiation, enabling the Reactor to access external services or repositories as needed.

go
// Facade, AggregateRepository or any other dependency can be passed from the caller
func NewReactor(fc comby.Facade, ... ) *Reactor { 
	rm := &Reactor{}
	rm.BaseReadmodel = comby.NewBaseReadmodel(fc.GetEventRepository())
	rm.Domain = "Order"
	rm.Name = "SimpleOrderReactor"

	// register domain event handlers
	comby.AddDomainEventHandler(rm, rm.SendEmailWhenOrderPlaced)
	return rm
}

The NewReactor function initializes a new Reactor instance:

It sets the Domain ("Order") and assigns a unique Name ("SimpleOrderReactor"). It registers the domain event handler SendEmailWhenOrderPlaced using comby.AddDomainEventHandler, ensuring the Reactor listens for OrderPlacedEvent events.

go
func (rm *Reactor) SendEmailWhenOrderPlaced(ctx context.Context, evt comby.Event, domainEvt *aggregate.OrderPlacedEvent) error {
	// send email to customer ...
	return nil
}

The SendEmailWhenOrderPlaced method defines the behavior for handling the OrderPlacedEvent. When this event is received. The Reactor processes the event, extracting the necessary information. It triggers an action, such as sending an email to the customer.

go
func (rm *Reactor) RestoreState(ctx context.Context, restoreFromZero bool) (comby.RestorationDoneCh, error) {
	// do not restore state in any reactor
	return nil, nil
}

Unlike typical readmodels, Reactors do not maintain state or need to restore state from historical events. The RestoreState method is overridden to ensure no restoration logic is applied.