Microservices

Sagas in microservices

We all know that the shared data in microservices(if they are done right) is eventually consistent. This is due to the CAP theorem which states that availability is usually a better choice than consistency. In a previous post I wrote about having eventual consistency data using Eventuate.

Since distributed transactions(2PC) are heavy with a bad performance, it was needed for something else, more lighter. Best effort 1PC is a lighter alternative. For example there is a ChainedTransactionManager in spring, but this comes with its problems. As the doc says “The configured instances will start transactions in the order given and commit/rollback in reverse order, which means the PlatformTransactionManager most likely to break the transaction should be the last in the list configured.” This means if the most outer transaction breaks first we haven’t achieved anything. It works under the assumption that “that errors causing a transaction rollback will usually happen before the transaction completion or during the commit of the most inner PlatformTransactionManager.

Now let’s concentrate on a different solution and that is the saga pattern. Yes the proposal is from 1987. A saga consists from a sequence of transactions, each transaction being triggered by the completion of the previous one. There are also compensation transactions that returns the data to a eventually consistent state. The latter ones are triggered in something goes wrong.  A service can have one or more sagas.

saga

Let’s take the following example. In the above image we have 2 services: Bid Service and Payment Service. After your bid is the winner you must pay for the item you bid. So the Bid Service is initiating a transaction that spans across Payment Service. Within the boundaries of a services we have local transactions so ACID is present. The transaction(blue arrow) that spans the services is a distributed transaction so here we have BASE. With ACID we have atomicity and consistency so we are covered here. With BASE with have eventual consistency so here the saga comes into play.

Let’s put this in practice using Eventuate Sagas. This uses an orchestrator component which issues the saga.

@Service
public class BidService {

    private BidRepository bidRepository;

    @Autowired
    private SagaManager bidPaymentSagaSagaManager;

    @Autowired
    public BidService(BidRepository bidRepository) {
        this.bidRepository = bidRepository;
    }

    @Transactional
    public Bid payForBid(Bid bid) {
        BidPaymentSagaData data = new BidPaymentSagaData(bid.getItemCode(), bid.getAmount());
        bidRepository.save(bid);
        bidPaymentSagaSagaManager.create(data, Bid.class, bid.getItemCode());
        return bid;
    }

    public Optional find(String itemCode) {
        return Optional.ofNullable(bidRepository.findByItemCode(itemCode));
    }

    public void update(Bid bid) {
        bidRepository.save(bid);
    }
}

It works with commands. The Bid Service is initiating a payment command which will be handled by the Payment Service Command Handler.

public class PaymentCommandHandler {

    @Autowired
    private PaymentService paymentService;

    public CommandHandlers commandHandlerDefinitions() {
        return SagaCommandHandlersBuilder
                .fromChannel("paymentService")
                .onMessage(InitiatePaymentCommand.class, this::pay)
                .build();
    }

    private Message pay(CommandMessage commandMessage) {
        InitiatePaymentCommand cmd = commandMessage.getCommand();
        Payment payment = new Payment(cmd.getItemCode(), cmd.getAmount());
        try {
            paymentService.pay(payment);
            return withSuccess();
        } catch (Exception e) {
            payment.reject();
            return withFailure();
        }
    }
}

This will respond either with success or failure. According to the saga we must trigger the next participant or a compensating transaction. The compensating transactions are triggered in reverse order.

public class BidPaymentSaga implements SimpleSaga {

    @Override
    public SagaDefinition getSagaDefinition() {
        return step().withCompensation(this::reject)
                .step().invokeParticipant(this::initiatePayment)
                .step().invokeParticipant(this::approvePayment)
                .build();
    }

    public CommandWithDestination initiatePayment(BidPaymentSagaData data) {
        return send(new InitiatePaymentCommand(data.getItemCode(), data.getAmount()))
                .to("paymentService")
                .build();
    }

    public CommandWithDestination approvePayment(BidPaymentSagaData data) {
        return send(new ApprovePaymentCommand(data.getItemCode()))
                .to("bidService")
                .build();
    }

    public CommandWithDestination reject(BidPaymentSagaData data) {
        return send(new RejectPaymentCommand(data.getItemCode()))
                .to("bidService")
                .build();
    }
}

Bid Payment Service is responsible for handling the outcome. The saga will send the corresponding message and the Bid Payment Command Handler will act accordingly. If the payment fails the bid will be rejected, and this means that we need a retry mechanism. But this is not in the scope of this post.

public class BidCommandHandler {

    @Autowired
    private BidService bidService;

    public CommandHandlers commandHandlerDefinitions() {
        return SagaCommandHandlersBuilder
                .fromChannel("bidService")
                .onMessage(RejectPaymentCommand.class, this::rejectBid)
                .onMessage(ApprovePaymentCommand.class, this::bidPayed)
                .build();
    }

    private Message rejectBid(CommandMessage cmd) {
        RejectPaymentCommand rejectPaymentCommand = cmd.getCommand();
        Optional bid = bidService.find(rejectPaymentCommand.getItemCode());
        if (bid.isPresent()) {
            bid.get().reject();
            bidService.update(bid.get());
        }
        return withFailure();
    }

    private Message bidPayed(CommandMessage cmd) {
        ApprovePaymentCommand approvePaymentCommand = cmd.getCommand();
        bidService.find(approvePaymentCommand.getItemCode()).ifPresent(Bid::approve);
        return withSuccess();
    }
}

This is a great way to ensure eventual consistency. Eventuate Sagas is based on two great products: Debezium for CDC (change data capture) which will tail the transaction log and Apache Kafka which is a well known message broker. The future is bright for these two.

SOURCE CODE

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s