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