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 aShipOrderCommand
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.
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.
// 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.
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.
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
.
- Assume an external initiator dispatches a command (Cmd1) to the Facade with
WaitForCmd
, which results in the creation of anAnyCreatedEvent
. - Assume the
Reactor
processes theAnyCreatedEvent
first among all EventHandlers. - The
Reactor
, in response to theAnyCreatedEvent
, generates and dispatches another command (Cmd2) to the Facade, also withWaitForCmd
. - The Facade processes Cmd2, leading to the creation of an
AnyUpdatedEvent
. - A different
Readmodel
listens to theAnyUpdatedEvent
and processes it accordingly. - After the
Reactor
completes its processing, the Facade continues to notify other readmodels about the originalAnyCreatedEvent
, 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.