Two-Phase Commit (2PC) in Microservices

The Two-Phase Commit (2PC) protocol is a technique used to ensure strong consistency across distributed systems. It involves coordinating multiple services to maintain atomicity in a transaction. 2PC is often used in financial transactions, inventory management, and other scenarios where immediate consistency is critical. In a microservices architecture, achieving such consistency is complex due to the distributed nature of the services and databases.

Why Are Distributed Transactions Challenging?

Microservices architecture introduces challenges in achieving consistency because:

  1. Multiple Databases: Each service typically has its own database, making it harder to maintain a single, consistent transaction across all databases.
  2. Network Latency: Network issues can lead to delays, making it challenging to maintain synchronization.
  3. Service Failures: If one service fails during a transaction, rolling back changes across all involved services is complex.

To address these challenges, 2PC ensures that all services either commit or abort the transaction simultaneously.

Two-Phase Commit Overview

The Two-Phase Commit (2PC) works in two distinct phases:

  1. Preparation Phase (Voting Phase):
    • The coordinator sends a prepare request to all participating services.
    • Each service checks whether it can commit and responds to the coordinator with either a Yes (prepared) or No (not prepared).
    • If any service responds with a No, the transaction is aborted.
  2. Commit Phase (Decision Phase):
    • If all services respond with Yes, the coordinator sends a commit request.
    • If any service responds with No, the coordinator sends an abort request to all services.

Detailed Workflow of 2PC

Two-Phase Commit in an E-Commerce System

Imagine an e-commerce application where a customer places an order. This transaction involves three services:

  1. Order Service: Manages the creation of the order.
  2. Inventory Service: Manages inventory and reserves stock.
  3. Payment Service: Processes the customer’s payment.

The Two-Phase Commit ensures that all three services either commit the transaction or abort it together, maintaining atomicity.

Preparation Phase Explained

In the Preparation Phase, each participating service performs the following steps:

  1. Local Validation: Each service checks whether it can fulfill the request:
    • The Order Service checks if the order is valid and can be created.
    • The Inventory Service checks if the requested item is available in stock.
    • The Payment Service checks if the payment information is valid.
  2. Prepare Response:
    • If the local checks pass, the service responds with Prepared (Yes) to the coordinator.
    • If any check fails, the service responds with Not Prepared (No), indicating that the transaction cannot proceed.
  3. Resource Locking:
    • During the preparation phase, each service locks the necessary resources (e.g., database rows, stock items) to prevent other transactions from modifying the data until the transaction is either committed or aborted.
    • This ensures data integrity during the transition.

Commit Phase Explained

Once all participating services are prepared, the coordinator decides whether to commit or abort based on the responses:

  1. Commit Request:
    • If all services responded with Prepared (Yes), the coordinator sends a commit request to all services.
    • Each service then completes the transaction, updates its local database, and releases the resource locks.
  2. Abort Request:
    • If any service responded with Not Prepared (No), the coordinator sends an abort request to all services.
    • Each service rolls back any changes made during the preparation phase and releases any locked resources.

Implementing Two-Phase Commit in Node.js

Let’s dive into a practical implementation of 2PC using Node.js for three services: Order, Inventory, and Payment.

1. Order Service

The Order Service acts as the coordinator in the 2PC protocol.

// orderService.js
const express = require('express');
const axios = require('axios');

const app = express();
app.use(express.json());

app.post('/startTransaction', async (req, res) => {
  try {
    // Phase 1: Preparation Phase
    const inventoryResponse = await axios.post('http://localhost:3001/prepare');
    const paymentResponse = await axios.post('http://localhost:3002/prepare');

    if (inventoryResponse.data === 'Prepared' && paymentResponse.data === 'Prepared') {
      // Phase 2: Commit Phase
      await axios.post('http://localhost:3001/commit');
      await axios.post('http://localhost:3002/commit');
      res.status(200).send('Transaction committed');
    } else {
      // Abort if any service fails
      await axios.post('http://localhost:3001/abort');
      await axios.post('http://localhost:3002/abort');
      res.status(500).send('Transaction aborted');
    }
  } catch (error) {
    // Handle errors by aborting the transaction
    await axios.post('http://localhost:3001/abort');
    await axios.post('http://localhost:3002/abort');
    res.status(500).send('Transaction failed and aborted');
  }
});

app.listen(3000, () => console.log('Order Service running on port 3000'));

2. Inventory Service

// inventoryService.js
const express = require('express');
const app = express();
app.use(express.json());

let prepared = false;

app.post('/prepare', (req, res) => {
  // Simulate preparation check
  prepared = true;
  res.send('Prepared');
});

app.post('/commit', (req, res) => {
  if (prepared) {
    // Simulate commit
    res.send('Committed');
  } else {
    res.status(400).send('Not prepared to commit');
  }
});

app.post('/abort', (req, res) => {
  // Simulate abort action
  prepared = false;
  res.send('Aborted');
});

app.listen(3001, () => console.log('Inventory Service running on port 3001'));

3. Payment Service

// paymentService.js
const express = require('express');
const app = express();
app.use(express.json());

let prepared = false;

app.post('/prepare', (req, res) => {
  // Simulate preparation check
  prepared = true;
  res.send('Prepared');
});

app.post('/commit', (req, res) => {
  if (prepared) {
    // Simulate commit
    res.send('Committed');
  } else {
    res.status(400).send('Not prepared to commit');
  }
});

app.post('/abort', (req, res) => {
  // Simulate abort action
  prepared = false;
  res.send('Aborted');
});

app.listen(3002, () => console.log('Payment Service running on port 3002'));

Sequence Diagram: Two-Phase Commit

πŸ’³ Payment ServiceπŸ›’ Inventory ServiceπŸ“¦ Order Service (Coordinator)πŸ‘€ UserπŸ’³ Payment ServiceπŸ›’ Inventory ServiceπŸ“¦ Order Service (Coordinator)πŸ‘€ Useralt[All services prepared][Any service not prepared]Place OrderPreparePreparedPreparePreparedCommitCommitCommittedCommittedOrder confirmedAbortAbortAbortedAbortedOrder failed

Key Concepts of 2PC

  1. Coordinator: Centralized control by the Order Service, which manages the transaction phases.
  2. Resource Locking: During the preparation phase, services lock necessary resources to maintain integrity.
  3. Synchronous Protocol: All services must be in sync for the transaction to succeed or fail.

Strengths and Weaknesses of 2PC

Strengths

  • Strong Consistency: Guarantees all-or-nothing transactions.
  • Atomicity: Ensures atomic behavior across services.
  • Predictability: Simplifies the logic for ensuring data consistency.

Weaknesses

  • Blocking: If a participant crashes during the commit phase, other participants may be blocked until the crashed service recovers.
  • Performance Overhead: The protocol's synchronous nature adds latency, making it slower than other patterns like SAGA.
  • Single Point of Failure: The coordinator is a potential point of failure. If it crashes, the transaction might be left in an uncertain state.

FAQs

Q1: What is the role of the coordinator in 2PC?

  • The coordinator manages the two phases: preparation and commit. It decides whether to commit or abort based on the responses from participants.

Q2: Why is 2PC considered blocking?

  • During the commit phase, if a participant fails, other participants

are blocked until the failure is resolved or the transaction is aborted.

Q3: Can 2PC be used in high-traffic systems?

  • 2PC is not ideal for high-traffic systems due to its synchronous nature, which adds latency. It’s better suited for critical transactions that require strong consistency.

Q4: How is resource locking managed in 2PC?

  • Resources are locked during the preparation phase to prevent conflicts. If the transaction is aborted, the locks are released.

Q5: What happens if the coordinator fails during 2PC?

  • If the coordinator fails during the commit phase, the system can enter an uncertain state where some participants may have committed and others may not have, potentially requiring manual intervention.

Q6: How does 2PC compare to SAGA?

  • While SAGA is designed for eventual consistency, 2PC ensures strong consistency by synchronizing all participants. SAGA is more scalable and less blocking, whereas 2PC is more suitable for critical transactions.

Q7: How does 2PC handle network partitions?

  • In case of network partitions, 2PC might fail to receive responses from all participants, causing the coordinator to abort the transaction to maintain consistency.

Q8: What is the advantage of 2PC over other patterns?

  • The main advantage is the strong consistency it provides, making it ideal for financial transactions or other operations where inconsistency is unacceptable.

Q9: Can 2PC be combined with SAGA?

  • Yes, 2PC can be combined with SAGA in hybrid architectures where some services require strong consistency while others can tolerate eventual consistency.

Q10: Is 2PC suitable for microservices architectures?

  • 2PC is suitable for microservices, but it comes with trade-offs in terms of scalability, blocking behavior, and complexity.

Conclusion

The Two-Phase Commit (2PC) protocol offers strong consistency and atomicity, making it suitable for distributed transactions that cannot tolerate inconsistencies. While it has limitations like blocking and performance overhead, it is still a reliable choice for financial transactions and other critical operations. However, for applications that require higher scalability and can handle eventual consistency, other patterns like SAGA or Three-Phase Commit (3PC) might be more suitable.

Clap here if you liked the blog