Skip to content
Stefan Domnanovits edited this page Aug 1, 2021 · 9 revisions

To create a saga all there is to do is implementing the Saga interface. Consider something like the following (very simplified) order workflow.

As the order arrives from a client the credit card information needs to be verified first. Once verification has successfully finished the order is forwarded from processing to the warehouse. The next example shows how this can be implemented using the saga-lib. Please note that there is no timeout handling. Timeouts are described in detail on the Timeouts pages.

First we need to think of the state that is saved between the different messages. A saga state is a class that holds all the information that needs to be shared when handling different messages. This state is stored by the saga lib and recovered later when needed again. By default this class instance is stored in memory but the saga-lib makes it easy to save it into any kind of persistent data storage to be available even after fail-over scenarios. This can be done by implementing the StateStorage interface. (s. Initialize page).

The example state below saves two different things; the originating orderRequestId and the originator where the request came from. Later on the id of billing request is also stored. However is not an explicit property. Rather it is added to a provided set of the super class because it is used as the instance key of the state. This can be seen where the key is explicitly added to the state using addInstanceKey() inside the saga.

/**
 * The state saved between the different steps of the saga.
 */
public class OrderState extends AbstractSagaState<String> {
    private String orderRequestId;
    private Originator originator;

    public String getOrderRequestId() {
        return orderRequestId;
    }

    public void setOrderRequestId(final String requestId) {
        orderRequestId = requestId;
    }

    public Originator getOriginator() {
        return originator;
    }

    public void setOriginator(final Originator originator) {
        this.originator = originator;
    }
}

After the saga state has been declared we can implement our saga. The saga inherits from AbstractSaga which already implements some of the necessary methods from of the Saga interface. When needed needed it is perfectly fine to provide a completely new implementation of Saga but most of the time You should be fine by inheriting from either AbstractSaga or AbstractSingleEventSaga.

The saga has two direct dependencies one to the Billing and one to the Warehouse service. Those are injected in the constructor. When using a DI framework supporting JSR-330 like Guice or Spring the dependencies will be automatically resolved because of the @Inject annotation on the constructor.

Next there are two handlers. One is annotated with @StartsSaga, the other with @EventHandler. @StartsSaga indicates a new saga is to be started for this messages. This results in a new saga state with a new id to be created. If the saga is not finished after processing this handler the new state will be saved in the target storage. To finish a saga call the setFinished() method like it is shown the cardVerfied below.

The cardVerfied handler does not create a new state. The message needs to be handled in the context of an existing state. Therefore the method is annotated with @EventHandler. Before the method ends setFinished() is called. This marks the saga as completed resulting in the deletion of the state. A saga can have any number of different event handlers and can implement a lot more complex scenarios as shown below.

public class OrderProcessSaga extends AbstractSaga<OrderState> {
    private final Billing billing;
    private final Warehouse warehouse;

    @Inject
    public OrderProcessSaga(final Billing billing, final Warehouse warehouse) {
        this.billing = billing;
        this.warehouse = warehouse;
    }

    @StartsSaga
    public void orderPlaced(final PlaceOrder orderRequest) {
        String billingRequestId = billing.verifyCredidCard(orderRequest.getCardNumber());

        state().setOrderRequestId(orderRequest.getRequestId());
        state().setOriginator(orderRequest.getOriginator());
        state().addInstanceKey(billingRequestId);
    }

    @EventHandler
    public void cardVerified(final CardVerified verifiedResponse) {
        OrderAccepted response = new OrderAccepted();
        response.setRequestId = state().getOrderRequestId();
        state().getOriginator().send(response);

        setFinished();
    }

    @Override
    public void createNewState() {
        setState(new OrderState());
    }

    @Override
    public Collection<KeyReader> keyReaders() {
        Collection<KeyReader> readers = new ArrayList<>(1);
        readers.add(KeyReaders.forMessage(
                CardVerified.class,
                verifiedMessage -> verifiedMessage.getRequestId()));

        return readers;
    }
}

There are two additional methods inside the saga. createNewState() is required from the Saga interface and creates a new state instance. It is enough to create an empty instance. Basic properties on the state like the saga id will be automatically populated by the saga-lib.

The other method keyReaders() is a little bit more complicated. Key readers are used to extract the key from a message matching any message property or value to an existing saga state. Sometimes the messages to be handled implement a common interface or base class with a property than can be used for matching. In this case one can implement a common reader class returning the specific value. In other cases however it is not that simple. The example above assumes the message is defined in a 3rd party library and can not be changed. In this case the saga-lib provides a generic KeyReaders helper that takes a KeyExtractFunction callback handler. In the example above the callback function returns the requestId which is then used by the saga-lib to search for a matching saga state.

Clone this wiki locally