A saga’s lifecycle isn’t about states; it’s about time and events dictating when it stops caring.
Imagine a customer places an order. This is an event. The system doesn’t just have an order; it processes an order. Sagas are the orchestrators of these processes, reacting to events and issuing commands. Let’s see this in action.
Here’s a simplified OrderSaga reacting to an OrderCreatedEvent:
@Saga
public class OrderSaga {
@Autowired
private transient CommandGateway commandGateway;
private String orderId;
private String paymentId;
private String shipmentId;
private boolean orderCompleted = false;
@StartSaga
@SagaEventHandler(associationProperty = "orderId")
public void handle(OrderCreatedEvent event) {
this.orderId = event.getOrderId();
this.paymentId = "payment-" + orderId;
this.shipmentId = "shipment-" + orderId;
// Command to initiate payment
commandGateway.send(new InitiatePaymentCommand(paymentId, orderId, event.getTotalAmount()));
}
@SagaEventHandler(associationProperty = "paymentId")
public void handle(PaymentInitiatedEvent event) {
// Command to initiate shipment
commandGateway.send(new InitiateShipmentCommand(shipmentId, orderId, event.getShippingAddress()));
}
@SagaEventHandler(associationProperty = "shipmentId")
public void handle(ShipmentInitiatedEvent event) {
// Mark order as completed and end the saga
this.orderCompleted = true;
// Command to mark order as completed
commandGateway.send(new CompleteOrderCommand(orderId));
// Explicitly end the saga
// You could also just let it naturally end if no more event handlers are present
// and no commands are being sent.
// For clarity, we can explicitly end it if we know there are no further steps.
// In Axon 4.x+, you'd use `GenericEventMessage.asEventMessage(new SagaLifecycle.endSaga())`
// or similar mechanism depending on your Axon version and setup.
// For this example, we'll assume it naturally ends after `CompleteOrderCommand`.
}
// ... other event handlers for failures, cancellations, etc.
}
This saga starts when an OrderCreatedEvent arrives. It then issues an InitiatePaymentCommand. Once PaymentInitiatedEvent is received, it issues an InitiateShipmentCommand. Finally, upon ShipmentInitiatedEvent, it issues a CompleteOrderCommand and logically ends its journey. The associationProperty is crucial: it links incoming events to a specific saga instance.
The problem this solves is managing complex, multi-step business processes that span multiple aggregates. Instead of trying to cram all the logic into a single aggregate (which quickly becomes unmanageable and violates single responsibility), sagas orchestrate the interaction between different aggregates by reacting to events and issuing commands.
Internally, Axon manages sagas by serializing their state and persisting it. When an event arrives, Axon finds the relevant saga instance (using the associationProperty), deserializes its state, invokes the appropriate event handler, updates the saga’s state, and persists it again. Commands are sent via the CommandGateway. The transient keyword on CommandGateway is important; it signifies that this field should not be serialized with the saga’s state, as it’s a service to be injected.
The key levers you control are:
- Saga Identification: How you link events to specific saga instances. This is done via
associationPropertyor by explicitly definingSagaIdentifiers in your event payloads. - Saga Lifecycle: When a saga starts (
@StartSaga) and when it ends. Ending a saga is crucial for resource management; an active saga consumes resources. You can end a saga implicitly by having no more event handlers or explicitly by sending a message that signals the end (e.g.,SagaLifecycle.endSaga()). - Event Handling: Which events your saga listens to and how it reacts.
- Command Issuance: What commands your saga sends to other aggregates to drive the process forward.
The most surprising thing about sagas is that they are inherently stateful and event-driven, but their "state" isn’t a set of entity attributes in the traditional sense. It’s the history of events processed and the commands yet to be issued. A saga’s state is effectively a compact representation of the business process it’s orchestrating, derived solely from the events it has seen.
Consider the CompleteOrderCommand in the ShipmentInitiatedEvent handler. This command is sent after the shipment is confirmed. In many systems, you might think of "order completion" as a final state of the Order aggregate. However, the saga’s job isn’t just to transition the Order aggregate; it’s to ensure the entire process (payment, shipping, confirmation) has occurred successfully. If the CompleteOrderCommand fails, the saga might then issue a CancelOrderCommand or a RefundPaymentCommand, depending on its defined logic for handling such failures. The saga’s lifecycle is about the process, not just the final state of a single aggregate.
The next logical step is handling failures and complex branching logic within your sagas.