Where is the Complexity? Part 3

Text Size 100%:

In my last blog post, I showed how one can use sagas to solve data consistency issues in a microservice based application.  However, the post also pointed out how much additional complexity is added on top of the business logic to support sagas.  The participants need to track that they are part of a saga and record enough information to be able to complete or compensate the saga when asked by the saga coordinator.  Even with this additional complexity, the sample saga code still has an issue with the lack of isolation.  As I explained, without providing some sort of escrow, it is possible that a participant may be asked to compensate but is unable to due to an intervening saga’s actions.  This leaves the application in an inconsistent state that will likely require manual intervention to resolve if possible.  In this post I’ll cover an alternative to sagas that provides much stronger consistency guarantees with far less effort and complexity for the developer.

Doing a little reading on the Internet about data consistency and microservices and one is led to believe that distributed transactions are to be avoided.  In particular XA distributed transactions are to be avoided due to claimed complexity, performance, and availability issues.  However, with XA the complexity is not in the application, but in the transaction manager, which presumably is provided by someone else that takes on that complexity.  From the developer’s perspective, XA is probably the easiest transaction pattern to use.  All that’s fundamentally required in the application code is to demarcate the boundaries of the transaction, which can simply be done using annotations.  Any performance issues due to locking conflicts can largely be dealt with by proper design and avoiding things like hot keys.  Here is an updated diagram for the simple Teller application when using XA:

XA Transaction Pattern

 

Which looks more complex than the diagram in the first post in this series.  However, the complexity is not in the application code, but in the supporting infrastructure.  In this case, the MicroTx libraries and transaction manager are used to hide this complexity.

Adding XA support to your microservices

Let’s look at the changes necessary to the simple application presented in the first post of this series.  The teller microservice providing the transfer function needs to demarcate the transaction boundaries.  To do this, one can either add an @Transactional annotation if supported by the framework being used, or create a new user transaction and call begin() to start the transaction and then either call commit() or rollback() to finish the transaction.  This is similar in terms of effort to the changes required for saga initiators, so the complexity for the transaction initiator is comparable in both patterns for initiators.  Here is the updated TransferResource.java code:

    /**
     * Transfer method annotated with @Transactional
     * If there are any exceptions, transaction rolls-back else commits the distributed transaction
     *
     * @param transferDetails
     * @return ResponseEntity with HTTP Response 200 on successful transfer. If there is any exception REST call returns 500.
     * @throws Exception
     */
    @RequestMapping(value = "transfer", method = RequestMethod.POST)
    @Transactional(propagation = Propagation.REQUIRED)
    public ResponseEntity<?> transfer(@RequestBody Transfer transferDetails) throws Exception {
        ResponseEntity<String> withdrawResponse = null;
        ResponseEntity<String> depositResponse = null;

        LOG.info("Transfer initiated: {}", transferDetails);

        withdrawResponse = withdraw(transferDetails.getFrom(), transferDetails.getAmount());
        if (!withdrawResponse.getStatusCode().is2xxSuccessful()) {
            LOG.error("Withdraw failed: {} Reason: {}", transferDetails, withdrawResponse.getBody());
            throw new TransferFailedException(String.format("Withdraw failed: %s Reason: %s", transferDetails, withdrawResponse.getBody()));
        }

        depositResponse = deposit(transferDetails.getTo(), transferDetails.getAmount());
        if (!depositResponse.getStatusCode().is2xxSuccessful()) {
            LOG.error("Deposit failed: {} Reason: {} ", transferDetails, depositResponse.getBody());
            throw new TransferFailedException(String.format("Deposit failed: %s Reason: %s ", transferDetails, depositResponse.getBody()));
        }

        LOG.info("Transfer successful: {}", transferDetails);
        return ResponseEntity
                .ok(new TransferResponse("Transfer completed successfully"));
    }

You can see that we added the @Transactional annotation to indicate that this method must be part of an XA transaction.  We also removed the redepositWithdrawnAmount() method as it isn’t necessary with XA transactions.  The amount withdrawn will automatically be put back if the transaction rolls back.  If this method returns successfully, the transaction will be committed.  If it throws an exception the transaction will be rolled back.

Where things get far simpler using XA transactions is in the participants.  With XA, all that’s required on the participant side is to use an injected resource manager connection, or an injected entity manager depending upon the framework being used and include the MicroTx libraries.  If we look at the updated participant code to use XA, we can see in AccountOperationService.java:

@Component
@RequestScope
public class AccountOperationService implements IAccountOperationService {

    private static final Logger LOG = LoggerFactory.getLogger(AccountOperationService.class);

    @Autowired
    @Qualifier("microTxEntityManager")
    @Lazy
    private EntityManager entityManager;

    @Autowired
    IAccountQueryService accountQueryService;

    @Override
    public void withdraw(String accountId, double amount) throws UnprocessableEntityException, NotFoundException {
        Account account = accountQueryService.getAccountDetails(accountId);
        if (account.getAmount() < amount) {
            throw new UnprocessableEntityException("Insufficient balance in the account");
        }
        LOG.info("Current Balance: " + account.getAmount());
        account.setAmount(account.getAmount() - amount);
        account = entityManager.merge(account);
        entityManager.flush();
        LOG.info("New Balance: " + account.getAmount());
        LOG.info(amount + " withdrawn from account: " + accountId);
    }

    @Override
    public void deposit(String accountId, double amount) throws NotFoundException {
        Account account = accountQueryService.getAccountDetails(accountId);
        LOG.info("Current Balance: " + account.getAmount());
        account.setAmount(account.getAmount() + amount);
        account = entityManager.merge(account);
        entityManager.flush();
        LOG.info("New Balance: " + account.getAmount());
        LOG.info(amount + " deposited to account: " + accountId);
    }
}

The business logic remains the same. All that changes is to use an injected entity manager provided by MicroTX.

Why Use XA instead of Sagas

With sagas, the participants need to provide completion and compensation callbacks that are application specific and can be quite complex as illustrated in my previous post.  With XA, the required callbacks are implemented in the MicroTx client libraries as they are independent of the application business logic.  The complexity sagas push onto the application developer is almost comletely eliminated by adopting the XA transaction pattern.

If we examine the complexity of using sagas vs XA, we see:

 

Sagas

XA

Demarcating transaction boundaries

Required

Required

Transaction completion logic

Developer provided

Not required

Transaction compensation logic

Developer provided

Not required

Transaction journaling

Developer provided

Not required

Transaction isolation (escrow)

Developer provided

Not required

Idempotent callbacks

Required

Provided

Conclusions and next steps

I close with the claim that using XA transactions is far easier and less complex and less error prone than using sagas.  If you value the consistency of your data and want your developers to be more productive, skip the hype around sagas and go straight to XA.

For more information on MicroTx, please visit the MicroTx homepage.  Ready to jump in?  Check out this quick start guide to ensuring the data consistency in your microservice based applications.

 

Todd Little

Chief Architect, Transaction Processing Products

I’m currently the Chief Architect for a family of transaction processing products at Oracle including Oracle Tuxedo product family, Oracle Blockchain Platform, and the new Oracle Transaction Manager for Microservices (MicroTx).  My main areas of focus are on security, privacy, confidentiality, performance, and scalability.  My job is to provide the technical strategy for these products to ensure they meet customer requirements.

Prior to being acquired by Oracle, I was Chief Architect for BEA Tuxedo at BEA Systems, Inc. While at BEA Systems, I was responsible for defining the technical strategy and direction for the Tuxedo product family. I developed the Tuxedo Control for WebLogic Workshop that greatly simplified the usage of Tuxedo services from Workshop based applications. I also received two patents for methods allowing design patterns in a UML modeling tool to control the generation of software artifacts.

During my nearly 50 years of software architecture and development experience, I have worked on a wide range of software systems and technology and have 44 published patents. At Science Applications International I worked on microcoded plasma display systems and command, control, and communication systems for naval applications. As a senior software consultant at Digital Equipment Corporation, I was the New York Area Regional Tools Consultant and also helped develop a multi-language multi-threaded distributed object oriented runtime environment with concurrent garbage collection.