Transactions in microservices
Building microservice is not an easy job in terms of transaction consistency and isolation as data ownership is decentralized. Let's explore the best practices for complex transaction coordination across multiple services.
Distributed transactions
Let's imagine that the user performs an action which on backend side triggers calls to multiple service and the result is aggregated and returned to the user. From the user perspective this is an atomic action, but each service is doing some part of the job in order to fulfill user's request. During execution of user action some of the service might fail to do the job, which leaves system in inconsistent state and you can't leave it like this.
The first approach which one can think of is to use two-phase commit. In this approach transaction manager is used to split operation across multiple resources into two phases: prepare and commit.
In theory, this is great, unfortunately in practice it's not that great. First, this approach implies synchronous execution where if the resource is unavailable, the transaction cannot be committed and must roll back. This increases number of retries and decreases availability of the system (overall). Also, a distributed transaction acquires a lock on the resource under transaction to ensure isolation, which increases the risk of deadlocks for long-running operations.
Event-based communication
Other approach how the problem can be solved using asynchronous communication, is by using events. Each service subscribes to event(s) of interest, on which service will react by performing specific work. Services are doing their work independently without knowing the overall outcome of the process.
Each of the service transaction is atomic, but not the complete transaction. So, during development we need to ensure that the system ultimately reaches a consistent state, even if some of the transactions fail. To ensure consistent state we can follow choreographed or orchestrated approach.
Choreographed approach
Pros:
- participating services do not need to know about each other
- loosely coupled services
- autonomous services
Cons:
- challenging validation
- increases complexity of state management
- potential cyclic dependency
- no clue where the process is in execution
Orchestrated approach
Pros:
- centralized sequencing
- track of where the process is
- reducing complexity of state management (of individual service)
Cons:
- moving too much logic to the coordinator
Unlike ACID transactions, event-based communication approaches aren't isolated. The result of each local transaction is immediately visible to other transactions affecting that particular entity. This means that a given entity might get simultaneously involved in multiple distinct actions. There are three common strategies for handling interwoven actions: short-circuiting, locking and interruption.
Transaction management in microservice architecture is a complex task as it involves coordinating transactions across multiple services, each with its own database and transactional boundaries. There are various approaches to tackle this challenge, however, each approach has its own trade-offs, including performance overhead, increased complexity, and potential for inconsistent data states. Choosing the right approach for transaction management depends on factors such as the use case, scalability requirements, and the level of consistency required.