Understanding Three-Phase Commit in Microservices

Introduction

In distributed systems, ensuring data consistency across multiple services can be quite challenging. The Three-Phase Commit (3PC) protocol is an enhancement of the Two-Phase Commit (2PC) protocol, designed to reduce the possibility of blocking situations during transaction commitment in a distributed environment. This protocol can be particularly useful in a microservices architecture to maintain consistency across services.

What is a Three-Phase Commit?

Three-Phase Commit is a distributed algorithm that ensures all participants in a transaction either commit or abort the transaction, achieving consensus without blocking in the event of certain failures. The protocol introduces an additional phase compared to 2PC, adding a "pre-commit" phase, which helps in further reducing the chances of blocking.

The protocol consists of three phases.

  1. CanCommit Phase: The coordinator sends a canCommit request to all participants to check if they can commit the transaction.
  2. PreCommit Phase: If all participants respond positively, the coordinator sends a pre-commit request to all participants to prepare for committing.
  3. DoCommit Phase: If all participants acknowledge the preCommit, the coordinator sends a doCommit request to all participants to finalize the transaction.

Implementing 3PC in Microservices using Java

To demonstrate the Three-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: OrderService and PaymentService.

Prerequisites

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

Step 1. Set Up the Project

Create a new Spring Boot project with the following dependencies.

  • Spring Web
  • Spring Boot Starter Data JPA
  • H2 Database

Step 2. Define the Microservices
 

OrderService

OrderServiceApplication.java

package com.threePhaseCommit.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.threePhaseCommit.OrderService.controller;

import com.threePhaseCommit.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("/canCommit")
    public String canCommitOrder(@RequestParam Long orderId) {
        // Logic to check if order can commit (e.g., check inventory)
        String response = orderService.canCommitOrder();
        return response;
    }

    @PostMapping("/preCommit")
    public String preCommitOrder(@RequestParam Long orderId) {
        // Logic to prepare the order (e.g., reserve inventory)
        String response = orderService.preCommitOrder();
        return response;
    }

    @PostMapping("/doCommit")
    public String doCommitOrder(@RequestParam Long orderId) {
        // Logic to commit the order (e.g., deduct inventory)
        String response = orderService.doCommitOrder();
        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.threePhaseCommit.OrderService.service;

import com.threePhaseCommit.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 doCommitOrder() {
        // Logic to commit the order (e.g., deduct inventory)
        return "DoCommit Order in Order Service";
    }

    public String canCommitOrder() {
        // Logic to check if order can commit (e.g., check inventory)
        return "Can Commit Order in Order Service";
    }

    public String preCommitOrder() {
        // Logic to prepare the order (e.g., reserve inventory)
        return "Pre Commit 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.threePhaseCommit.OrderService.repository;

import com.threePhaseCommit.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.threePhaseCommit.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

  1. For Can Commit Order.
    Commit Order
  2. For Do Commit Order.
    Do Commit Order
  3. For Pre-Commit Order.
    Pre-Commit Order
  4. For Abort Order.
    Abort Order

PaymentService

PaymentServiceApplication.java

package com.threePhaseCommit.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.threePhaseCommit.PaymentService.controller;

import com.threePhaseCommit.PaymentService.repository.PaymentRepository;
import com.threePhaseCommit.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("/canCommit")
    public String canCommitPayment(@RequestParam Long paymentId) {
        // Logic to check if payment can commit (e.g., check funds availability)
        String response = paymentService.canCommitPayment();
        return response;
    }

    @PostMapping("/preCommit")
    public String preCommitPayment(@RequestParam Long paymentId) {
        // Logic to prepare the payment (e.g., hold funds)
        String response = paymentService.preCommitPayment();
        return response;
    }

    @PostMapping("/doCommit")
    public String doCommitPayment(@RequestParam Long paymentId) {
        // Logic to commit the payment (e.g., transfer funds)
        String response = paymentService.doCommitPayment();
        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;
    }
}

PaymentService.java

package com.threePhaseCommit.PaymentService.service;

import com.threePhaseCommit.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 canCommitPayment() {
        return "DoCommit Payment in Payment Service";
    }

    public String preCommitPayment() {
        return "DoCommit Payment in Payment Service";
    }

    public String doCommitPayment() {
        return "DoCommit Payment in Payment Service";
    }

    public String abortPayment() {
        return "DoCommit Payment in Payment Service";
    }
}

Payment Repository.java

package com.threePhaseCommit.paymentService.repository;

import com.threePhaseCommit.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.threePhaseCommit.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
  1. For Pre Commit Payment.
    Payment Service
  2. For Can Commit Payment.
     Can Commit Payment
  3. For Do Commit Payment.
    Do Commit Payment
  4. For Abort Payment.
    Abort Payment

Coordinator Service

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

CoordinatorServiceApplication.java

package com.threePhaseCommit.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.threePhaseCommit.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.threePhaseCommit.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 3PC implementation, you can use the following endpoint.

POST http://localhost:8080/transaction?orderId=1&paymentId=1

Conclusion

Implementing the Three-Phase Commit protocol in a microservices architecture helps ensure data consistency across distributed services with a reduced risk of blocking compared to Two-Phase Commit. While 3PC adds complexity and overhead, it is useful in scenarios where blocking can cause significant issues. However, it is important to consider the trade-offs and evaluate if other patterns, such as the Saga pattern, might be more suitable for your specific use case.