Microservices & Distributed Transactions
The Single Responsibility Principle and the Database-per-Service pattern are two microservice patterns that allow building loosely coupled microservices that can be independently scaled and deployed. However, they bring in additional challenges. One of these challenges is how to make a request that requires co-ordination between multiple microservices atomic and consistent.
Say, your e-commerce application is composed of OrderService, InventoryService, CustomerService. When an order is submitted, inventory needs to be reserved from the inventory service, credit needs to be reserved from the customer service and if either of those could not happen, the order should not be submitted.
In a monolith where order, inventory and customer credit were all in the same database, a database transaction that includes all three updates would have ensured atomicity and consistency. Coordinating between microservices to achieve the same is a challenge. Two patterns that help bring atomicity and consistency to distributed transactions in microservices are Two Phase Commit and Saga pattern.
Two Phase Commit
Two Phase Commit is a standardized way by which most distributed databases achieve atomicity and consistency. It can be adapted to handle distributed transactions in microservices. It requires a central orchestrator to co-ordinate requests between multiple microservices; and in each microservice the operation happens in two phases – the prepare phase and the commit phase.
Two phase commit operates synchronously ie the orchestrator coordinates the request between multiple microservices and the final response is reported back to the client. The operation is atomic to the requestor – it either commits or rolls back. However, it is harder to implement in microservices since there is interdependency between the microservices and the orchestrator.
Saga Pattern /Eventual Consistency
The Saga pattern aims for eventual consistency. In this pattern the microservices communicate via events. The service that receives a request, updates the state in its database and publishes the state change via an event on the event bus. Other microservices react to the event by making necessary changes in a local transaction and responds to the original microservice.
When the OrderService receives the request for an order, it creates the order in pending state in its database and sends the OrderCreated event on the message bus.
The InventoryService receives the OrderCreated event and attempts to reserve the items and sends an InventoryReserved or InventoryReserveFailed message.
The CustomerService attempts to reserve credit and responds with CreditReserved or CreditReserveFailed message.
The OrderService receives the messages from OrderService and CustomerService and either approves or rejects the order based on the outcomes in InventoryService and CustomerService. If it rejects the order, it emits an OrderRejected message based on which the InventoryService and CustomerService will need to rollback any changes they had previously made.
This Saga pattern is easier to implement as each microservice can be developed independently. However, it guarantees only eventual consistency. So a customer’s credit limit may dip as it is reserved by the CustomerService and then go up again as the OrderService rejects the order since inventory could not be reserved.
Note: To ensure that the update of state in the local database and the publishing of the state change on the event bus happen atomically, you may need to implement another pattern called the transactional outbox pattern.
The Verdict
Whether to use Two Phase Commit or Saga pattern depends on your use case. Reactive microservices that implement the Saga pattern are definitely the way to go in most cases. If your use case does not tolerate eventual consistency, you may need to implement Two Phase Commit.
Good write up Pooja!
Excellent Pooja 👍