Understanding Two-Phase Commit in Microservices

Introduction

In a microservices architecture, maintaining data consistency across multiple services can be challenging. One common approach to ensure consistency is the Two-Phase Commit (2PC) protocol. This protocol is designed to coordinate a single transaction across multiple services, ensuring that either all services commit their changes or none of them do, thus maintaining data consistency.

What is a Two-Phase Commit?

The Two-Phase Commit protocol is a type of atomic commitment protocol that is used in distributed systems to achieve consensus on a transaction. The protocol consists of two phases:

  1. Prepare Phase: The coordinator sends a prepare request to all participants. Each participant performs all necessary actions and writes all changes to a transaction log but does not yet commit the changes. They then respond with a vote (commit or abort).
  2. Commit Phase: If all participants vote to commit, the coordinator sends a commit request to all participants. If any participant votes to abort, the coordinator sends an abort request. Each participant then either commits or aborts the transaction based on the coordinator's decision.

Implementing 2PC in Microservices using Java

To demonstrate the Two-Phase Commit protocol, we'll create a simplified example using Spring Boot. We'll implement two microservices that need to participate in a distributed transaction.

  1. OrderService
  2. PaymentService
  3. Coordinate Service

Prerequisites

  • Java Development Kit (preferred JDK 21)
  • Spring Boot
  • Maven

Step-by-step implementation

  1. Set Up the Project: Create a new Spring Boot project with the following dependencies.
    • Spring Web
    • Spring Boot Starter Data JPA
    • H2 Database
  2. Define the Microservices

1. OrderService

OrderServiceApplication.java

package com.twoPhaseCommit.orderservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

OrderController.java

package com.twoPhaseCommit.OrderService.controller;
import com.twoPhaseCommit.OrderService.service.OrderServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/order")
public class OrderController {

    @Autowired
    OrderServiceImpl orderService;

    @PostMapping("/prepare")
    public String prepareOrder(@RequestParam Long orderId) {
        // Logic to prepare the order (e.g., check inventory)
        String response = orderService.prepareOrder();
        return response;
    }
    @PostMapping("/commit")
    public String commitOrder(@RequestParam Long orderId) {
        // Logic to commit the order (e.g., deduct inventory)
        String response = orderService.commitOrder();
        return response;
    }
    @PostMapping("/abort")
    public String abortOrder(@RequestParam Long orderId) {
        // Logic to abort the order (e.g., release reserved inventory)
        String response = orderService.abortOrder();
        return response;
    }
}

OrderService.java

package com.twoPhaseCommit.OrderService.service;

import com.twoPhaseCommit.OrderService.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl {
    @Autowired
    OrderRepository orderRepository;

    //Your own logic for respective methods
    public String prepareOrder() {
        // Logic to prepare the order (e.g., check inventory)
        return "Prepared Order in Order Service";
    }

    public String commitOrder() {
        // Logic to commit the order (e.g., deduct inventory)
        return "Committed Order in Order Service";
    }

    public String abortOrder() {
        // Logic to abort the order (e.g., release reserved inventory)
        return "Aborted Order in Order Service";
    }
}

OrderRepository.java

package com.twoPhaseCommit.OrderService.repository;

import com.twoPhaseCommit.OrderService.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.UUID;

@Repository
public interface OrderRepository extends JpaRepository<Order, UUID> {
}

Order.java

package com.twoPhaseCommit.OrderService.entity;

import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.List;
import java.util.UUID;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {

    @Id
    @GeneratedValue(strategy= GenerationType.UUID)
    private UUID orderId;

    private String orderName;

    private String orderDesc;

    private Date createdAt;

    private Date updatedAt;

    private String createdBy;

    private String updatedBy;

    private List<Object> items;
}

application.properties file for Order Service

spring.application.name=OrderService
server.port=8081

Expected Output

  • For Prepare Order
     Prepare Order
  • For Commit Order
    Commit Order
  • For Abort Order
    Abort Order

2. Payment Service

PaymentServiceApplication.java

package com.twoPhaseCommit.paymentservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PaymentServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(PaymentServiceApplication.class, args);
    }
}

PaymentController.java

package com.twoPhaseCommit.PaymentService.controller;
import com.twoPhaseCommit.PaymentService.service.PaymentServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("api/v1/payment")
public class PaymentController {

    @Autowired
    PaymentServiceImpl paymentService;

    @PostMapping("/prepare")
    public String preparePayment(@RequestParam Long paymentId) {
        // Logic to prepare the payment (e.g., hold funds)
        String response = paymentService.preparePayment();
        return response;
    }
    @PostMapping("/commit")
    public String commitPayment(@RequestParam Long paymentId) {
        // Logic to commit the payment (e.g., transfer funds)
        String response = paymentService.commitPayment();
        return response;
    }
    @PostMapping("/abort")
    public String abortPayment(@RequestParam Long paymentId) {
        // Logic to abort the payment (e.g., release hold on funds)
        String response = paymentService.abortPayment();
        return response;
    }
}

PaymentServiceImpl.java

package com.twoPhaseCommit.PaymentService.service;

import com.twoPhaseCommit.PaymentService.repository.PaymentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PaymentServiceImpl {
    @Autowired
    PaymentRepository paymentRepository;

    //Your own logic for respective methods
    public String preparePayment() {
        // Logic to prepare the payment (e.g., hold funds)
        return "Prepared Payment in Payment Service";
    }

    public String commitPayment() {
        // Logic to commit the payment (e.g., transfer funds)
        return "Committed Payment in Payment Service";
    }

    public String abortPayment() {
        // Logic to abort the payment (e.g., release hold on funds)
        return "Aborted Payment in Payment Service";
    }
}

PaymentRepository.java

package com.twoPhaseCommit.paymentService.repository;

import com.twoPhaseCommit.paymentService.entity.payment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.UUID;

@Repository
public interface PaymentRepository extends JpaRepository<Payment, UUID> {
}

Payment.java

package com.twoPhaseCommit.PaymentService.entity;

import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.UUID;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Payment {

    @Id
    @GeneratedValue(strategy= GenerationType.UUID)
    private UUID paymentId;

    private String paymentGatewayName;

    private String paymentMode;

    private String paymentDesc;

    private Date createdAt;

    private Date updatedAt;

    private String createdBy;

    private String updatedBy;
}

application.properties file for Payment Service

spring.application.name=PaymentService
server.port=8082

Expected Output

  • For Prepare Payment
    Prepare Payment
  • For Commit Payment
    Commit Payment
  • For Abort Payment
     Abort Payment

3. Coordinator Service

Create a coordinator service to manage the two-phase commit process.

CoordinatorServiceApplication.java

package com.twoPhaseCommit.coordinatorservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CoordinatorServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(CoordinatorServiceApplication.class, args);
    }
}

CoordinatorController.java

package com.twoPhaseCommit.Coordinate_Service.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class CoordinatorController {
    @Autowired
    private RestTemplate restTemplate;
    
    @PostMapping("/transaction")
    public String performTransaction(@RequestParam Long orderId, @RequestParam Long paymentId) {
        String orderPrepareUrl = "http://localhost:8081/api/v1/order/prepare?orderId=" + orderId;
        String paymentPrepareUrl = "http://localhost:8082/api/v1/payment/prepare?paymentId=" + paymentId;
       
        ResponseEntity<String> orderResponse = restTemplate.postForEntity(orderPrepareUrl, null, String.class);
        ResponseEntity<String> paymentResponse = restTemplate.postForEntity(paymentPrepareUrl, null, String.class);
        
        if ("prepared".equals(orderResponse.getBody()) && "prepared".equals(paymentResponse.getBody())) {
            restTemplate.postForEntity("http://localhost:8081/api/v1/order/commit?orderId=" + orderId, null, String.class);
            restTemplate.postForEntity("http://localhost:8082/api/v1/payment/commit?paymentId=" + paymentId, null, String.class);
            return "Transaction committed";
        } else {
            restTemplate.postForEntity("http://localhost:8081/api/v1/order/abort?orderId=" + orderId, null, String.class);
            restTemplate.postForEntity("http://localhost:8082/api/v1/payment/abort?paymentId=" + paymentId, null, String.class);
            return "Transaction aborted";
        }
    }
}

RestTemplateConfig.java

package com.example.coordinatorservice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

Running the Application

  1. Start OrderService on port 8081.
  2. Start PaymentService on port 8082.
  3. Start CoordinatorService on port 8080.

To test the 2PC implementation, you can use the following endpoint.

Post: http://localhost:8080/transaction?orderId=101&paymentId=101

Conclusion

Implementing a Two-Phase Commit in a microservices architecture can help ensure data consistency across distributed services. While 2PC is effective for maintaining consistency, it is important to consider its drawbacks, such as increased latency and potential for blocking. In real-world applications, alternative approaches like Saga patterns might be considered depending on the use case.