https://blog.micromark.com/wp-content/uploads/2018/04/drakkar-ship-dragon-sailors-vikings-sea.jpg
A controversial architectural change we encounter with microservices architectures is the adoption of the Database per Service pattern. When using this pattern, an application is no longer built around a single database but instead is composed of many services each responsible for its own database. This move to a distributed data model is a key factor in providing much of the flexibility of microservice architectures. By segregating our data along service boundaries, we gain a high degree of isolation between services. With this isolation comes the flexibility for each service to implement its own private operations and persistence without having to coordinate with its consumers.

Unfortunately, when we distribute an application's data, we are no longer able to leverage foreign key constraints and the transactionality of the shared database to build consensus. Even though each service's internal implementation may employ transactions, we dont have ACID transaction semantics that span services. By distributing the service, we lose the Atomic property of ACID. We no longer have a guarantee that operations spanning multiple microservices will succeed completely. What microservices architectures need are Distributed Transactions.

Distributed Transactions provide a mechanism where two or more network hosts are involved in a transaction. In this article, we will look at two approaches: the Two-phase commit protocol, and the Saga pattern.

Two-phase Commit (2PC) protocol

The Two-phase commit protocol (2PC) protocol provides a mechanism for handling distributed transactions using a request phase and a commit phase. During the request phase, The transaction manager instructs all participating resources to prepare for a given operation. Each resource then attempts to prepare the operation for execution and returns a response indicating whether the operation can be successfully completed. The distributed transaction manager waits for a response from each participating resource, and once all resources have responded, The transaction manager decides whether the transaction should be completed (committed) or aborted (rollback).

The Two-phase commit protocol provides a strong consistency protocol for handling transactions. However, this approach is not generally recommended for microservices for several reasons. Firstly, the Two-Phase commit protocol attempts to manage the atomicity requirement by locking each resource during the transaction's prepare phase. While ensuring consistency, locking limits service availability by excluding other callers access during the transaction. In worst-case scenarios, two transactions can potentially lock each other's resources causing a deadlock condition to occur.

Additionally, Two-phase commit is synchronous in nature. The transaction manager must wait for a response from each participating resource before it can decide the fate of the transaction. As we've seen in past articles, synchronous operations negatively impact availability and are generally avoided with reactive microservice architectures.

Lastly, in addition to the problems caused by resource locking, a distributed transaction manager's performance characteristics differ significantly from its non-distributed cousins. Distributed transactions are subject to additional issues from network connectivity and communication latency problems. These network-related issues significantly impact transaction performance and contribute to the Two-phase commit protocol's unsuitability for microservice architectures.

Another way-The Saga Pattern

An alternative approach to distributed transactions is the Saga pattern. Unlike the two-phase commit protocol, The Saga pattern implements distributed transactions as a set of asynchronous local transactions for each participating service. For every local transaction, the Saga publishes an event to the message bus to trigger the subsequent local transaction. In the event of a local transaction failure, the Saga executes a series of compensating transactions to rollback all the previous local transactions.

The Saga Pattern transfers the responsibility for inter-service transactionality from the database-level to the application-level. A major advantage of the Saga is its ability to service long-lived transactions without blocking. This advantage is made possible as each service is only concerned with its own local transactions and any locking is performed at the internal persistence-level and not at the service level.

When implementing the Saga pattern we have the choice between two implementation strategies: Choreographed and Orchestrated.

Choreographed Sagas

The simplest implementation is the choreographed Saga. With a choreographed Saga, there is no central controller. Instead, when a service performs a local transaction, it also generates a service event that drives the next stage of the Saga. This process repeats until the Saga has completed each of its constituent steps. In addition to generating events, each participating transaction service listens for events that trigger its local transactions. When the service receives an event, it is evaluated, and it either executes its local transactions, or it issues the events necessary to initiate the appropriate compensating transactions to rollback any changes. Choreographed Sagas provide a simple, event-based mechanism that requires minimal implementation effort. A Choreographed Saga is appropriate for Sagas with fewer steps (less than five). As the number of steps increases, choreographed Sagas become increasingly complex and challenging to understand, manage and troubleshoot.

Orchestrated Sagas

The second approach we will look at, introduces more complexity to the implementation of a Saga through the addition of an Orchestrator service. The Orchestrator service acts as a state machine that drives each Saga. With orchestrated sagas, participating services are no longer responsible for listening for Saga-specific events. Instead, the orchestrator issues commands to services instructing them of the desired operation in response to its state changes and listens to their replies to update its state.

While adding the cost of an additional service to the application, the orchestrated Saga simplifies the Saga implementation at the service level. It accomplishes this by removing any Saga-specific code from each participating service and centralizing the sequence of operations as a state machine. This centralization makes the Saga easier to understand, implement and debug.

Summary

Supporting transaction semantics is critical to any non-trivial application. We've looked at two distributed transaction strategies, Two-phase commit protocol and Sagas and have seen that for microservice architectures, Sagas are the better fit.

Coming Up

We have seen how Sagas leverage domain events to provide distributed transaction support. In the next post, we will dig deeper into the world of Command, Events & Queries with Event Sourcing.