How do I manage a transaction between two commands?

Suppose I have two commands: creditCommand, debitCommand and transfer.
I want to implement a transfer method (debit the source account and credit the destination account)
Void transfer(){
debit(debitCommand)
credit(creditCommand)
}

HERE, the problem is that if credit fails (Exception), the debit method will already be executed correctly.
How can I rollback to cancel the debit command? How can we make this method transactional?
or should I implement an action to compensate for the debit method?

I think the best way to handle this would be to create a saga to manage this transfer interaction. You can have your rest controller or service publish an event directly using either the EventGateway or the EventBus. This event would hold the necessary data to create the debit and credit commands for the respective accounts and something like a transaction id to use as the association property of the saga.

In the first part of the saga you would issue the debit command. You will need to add the transactionId property to your debit command to pass to what I assume would be an AccountDebitedEvent. Then the saga would have an event handler for the AccountDebitedEvent and issue a credit command. Again you will need to add the transactionId to this command and the resulting AccountCreditedEvent. Then you can have a callback function for if the command errors to issue a RollbackDebitCommand which would “undo” the previous debit, being sure to have some form of final error handling or logging if that command were to fail. You would then have a final event handler for the AccountCreditedEvent that would end the saga for the transaction.

This is just one way that you could use a saga. Depending on your implementation you may want to do something like a ReserveDebitCommand first, that makes the funds unavailable but not necessarily taken from the source account, followed by the CreditCommand which when successful will issue a CaptureDebitCommand or something to that effect.

In general, it seems like what you’re looking for is a saga. If you want to see a more concrete example of one, you can see this one from one of my projects:

@Saga
public class AccountDeletionSaga implements Serializable {

    @Autowired
    private transient CommandGateway commandGateway;
    @Autowired
    private transient LoyaltyBankLookupRepository loyaltyBankLookupRepository;

    private static final Logger LOGGER = LoggerFactory.getLogger(AccountDeletionSaga.class);

    @SagaEventHandler(associationProperty = "accountId")
    @StartSaga
    public void handle(AccountDeletedEvent event) {
        String accountId = event.getAccountId();
        SagaLifecycle.associateWith("accountId", accountId);

        Marker marker = Markers.append(REQUEST_ID, event.getRequestId());
        LOGGER.info(marker, "{} received, checking for loyaltyBankEntities", event.getClass().getSimpleName());

        List<LoyaltyBankLookupEntity> loyaltyBankLookupEntities = loyaltyBankLookupRepository.findByAccountId(accountId);

        if (loyaltyBankLookupEntities.isEmpty()) {
            LOGGER.info(marker, "No loyaltyBankEntities found, ending saga");
            SagaLifecycle.end();
            return;
        }

        loyaltyBankLookupEntities.forEach(
                loyaltyBankLookupEntity -> {
                    ExpireAllPointsCommand command = ExpireAllPointsCommand.builder()
                            .requestId(event.getRequestId())
                            .loyaltyBankId(loyaltyBankLookupEntity.getLoyaltyBankId())
                            .build();

                    LOGGER.info(
                            MarkerGenerator.generateMarker(command),
                            "Sending {} command",
                            command.getClass().getSimpleName()
                    );

                    commandGateway.send(command);
                }
        );
    }

    @SagaEventHandler(associationProperty = "accountId")
    public void handle(AllPointsExpiredEvent event) {
        DeleteLoyaltyBankCommand command = DeleteLoyaltyBankCommand.builder()
                .requestId(event.getRequestId())
                .loyaltyBankId(event.getLoyaltyBankId())
                .build();

        Marker marker = MarkerGenerator.generateMarker(command);
        marker.add(Markers.append(REQUEST_ID, command.getRequestId()));
        LOGGER.info(marker, "{} received, issuing {}", event.getClass().getSimpleName(), command.getClass().getSimpleName());

        commandGateway.send(command);
    }

    @SagaEventHandler(associationProperty = "accountId")
    public void handle(LoyaltyBankDeletedEvent event) {
        Marker marker = Markers.append(REQUEST_ID, event.getRequestId());
        LOGGER.info(marker, "{} received, checking for loyaltyBankEntities", event.getClass().getSimpleName());

        List<LoyaltyBankLookupEntity> loyaltyBankLookupEntities = loyaltyBankLookupRepository.findByAccountId(event.getAccountId());

        if (loyaltyBankLookupEntities.isEmpty()) {
            LOGGER.info(marker, "No loyaltyBankEntities found, ending saga");
            SagaLifecycle.end();
            return;
        }

        LOGGER.info(
                marker, "{} loyaltyBankEntities remaining, continuing saga lifecycle",
                loyaltyBankLookupEntities.size()
        );
    }
}

This is for cleanup on an account deletion and I do not have any rollbacks at present, but hopefully this will point you in the right direction. Here is also a link to Axon’s documentation: Sagas

1 Like