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.

Cascading Effects

Reactors can create cascading effects by generating new commands in response to events they handle. This can lead to out of order event processing in other readmodels. With WaitForCmd this effect is further enhanced.

For example: There is a Reactor and a Readmodel. The Reactor only listens for AnyCreatedEvent, but generates a new event, AnyUpdatedEvent, with a new command and an additional WaitForCmd. The new generated event AnyUpdatedEvent is processed before AnyCreatedEvent due to the resulting nesting in the Readmodel.

  1. Assume an external initiator dispatches a command (Cmd1) to the Facade with WaitForCmd, which results in the creation of an AnyCreatedEvent.
  2. Assume the Reactor processes the AnyCreatedEvent first among all EventHandlers.
  3. The Reactor, in response to the AnyCreatedEvent, generates and dispatches another command (Cmd2) to the Facade, also with WaitForCmd.
  4. The Facade processes Cmd2, leading to the creation of an AnyUpdatedEvent.
  5. A different Readmodel listens to the AnyUpdatedEvent and processes it accordingly.
  6. After the Reactor completes its processing, the Facade continues to notify other readmodels about the original AnyCreatedEvent, allowing them to update their state as needed.

The event ordering in the Readmodel gets disrupted because AnyUpdatedEvent is processed before AnyCreatedEvent completes. This is a natural consequence of the Reactor's ability to generate new commands and events in response to existing ones and waiting for them to complete with WaitForCmd, which can lead to out-of-order event handling in other readmodels.

To solve the problem and to avoid the cascading effect, you can simply implement the GetOrder method in your Readmodel or Reactor. See Event Handler Orderer for more details.