Handling Distributed Transactions with CompletableFuture in Spring

  softwareengineering

I’m looking for an opinion about an approach for dealing with distributed transactions. In a Spring Boot program, how can I implement the following business process without sacrificing to code locality?

class PayForASubscriptionService {

    PaymentService paymentService;

    public PayForASubscriptionService(PaymentService paymentService) { 
        this.paymentService = paymentService; 
    }

    void registerWithMonthlyPayments(String customerEmail, String bankAccountID, Subscription subscriptionData) throws PaymentException {
        // Must wait for the next call to complete. The payment engine will advise us by a webhook.
        Wallet customerWallet = this.paymentService.createWallet(customerEmail, bankAccountID);

        // Must wait for the next call to complete. The payment engine will advise us by a webhook.
        this.paymentService.createPayment(customerWallet, subscriptionData.getDepositAmount());

        // Must wait for the next call to complete. The payment engine will advise us by a webhook.
        this.paymentService.createAMonthlyPaymentsSchedule(customerWallet, subscription.getMontlyPaymentAmount());

        this.emailer.send(subscriptionConfirmationTemplate, customerEmail, subscriptionData);
    }
}

The third-party paymentService will advise us with webhooks about the outcome of each API call.

When dealing with webhooks, we normally need to define callbacks for handling the outcome of asynchronous calls (like paymentService.createWallet, paymentService.createPayment, and paymentService.createAMonthlyPaymentsSchedule). Hence, each of the above calls would be part of the interface of a REST controller. The problem with this approach is that it would force me to disseminate the PayForASubscriptionService.registerWithMonthlyPayments‘s logic into many callbacks.

I was considering to use CompletableFuture for doing asynchronous calls:

    void registerWithMonthlyPayments(String customerEmail, String bankAccountID, Subscription subscriptionData) throws PaymentException {

        CompletableFuture<Wallet> walletFuture = this.paymentService.createWallet(customerEmail, bankAccountID, separateThread);

        // We use the thenCompose() method to chain two Futures sequentially (see the monadic design pattern).
        walletFuture.thenCompose(wallet -> this.paymentService.createPayment(wallet, subscriptionData.getDepositAmount()))
                    .thenCompose(payment -> this.paymentService.createAMonthlyPaymentsSchedule(walletFuture.join(), subscription.getMontlyPaymentAmount()))

                    // We use thenRun() if we neither need the value of the computation nor want to return some value at the end of the chain.
                    .thenRun(() -> this.emailer.send(subscriptionConfirmationTemplate, customerEmail, subscriptionData))

                    .exceptionally(ex -> {
                        // Handle exception
                        return null;
                    });
    }

Considering that a payment would require up to 7 days before we receive a successful confirmation, my concern with this option would be the memory footprint required for such calls (even when they’re waiting to be awakened). Furthermore, I’m not sure that CompletableFuture is meant to be active that long.

Thanks for the input

LEAVE A COMMENT