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-by-Step Implementation
 

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.example.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);
    }
}

Java

Copy

OrderController.java

package com.example.orderservice.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @PostMapping("/order/canCommit")
    public String canCommitOrder(@RequestParam Long orderId) {
        // Logic to check if order can commit (e.g., check inventory)
        return "yes";
    }

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

    @PostMapping("/order/doCommit")
    public String doCommitOrder(@RequestParam Long orderId) {
        // Logic to commit the order (e.g., deduct inventory)
        return "committed";
    }

    @PostMapping("/order/abort")
    public String abortOrder(@RequestParam Long orderId) {
        // Logic to abort the order (e.g., release reserved inventory)
        return "aborted";
    }
}

Java

Copy

PaymentService

PaymentServiceApplication.java

package com.example.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);
    }
}

Java

Copy

PaymentController.java

package com.example.paymentservice.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PaymentController {

    @PostMapping("/payment/canCommit")
    public String canCommitPayment(@RequestParam Long paymentId) {
        // Logic to check if payment can commit (e.g., check funds availability)
        return "yes";
    }

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

    @PostMapping("/payment/doCommit")
    public String doCommitPayment(@RequestParam Long paymentId) {
        // Logic to commit the payment (e.g., transfer funds)
        return "committed";
    }

    @PostMapping("/payment/abort")
    public String abortPayment(@RequestParam Long paymentId) {
        // Logic to abort the payment (e.g., release hold on funds)
        return "aborted";
    }
}

Java

Copy

Coordinator Service

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

CoordinatorServiceApplication.java

package com.example.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);
    }
}

Java

Copy

CoordinatorController.java

package com.example.coordinatorservice.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 orderCanCommitUrl = "http://localhost:8081/order/canCommit?orderId=" + orderId;
        String paymentCanCommitUrl = "http://localhost:8082/payment/canCommit?paymentId=" + paymentId;

        ResponseEntity<String> orderResponse = restTemplate.postForEntity(orderCanCommitUrl, null, String.class);
        ResponseEntity<String> paymentResponse = restTemplate.postForEntity(paymentCanCommitUrl, null, String.class);

        if ("yes".equals(orderResponse.getBody()) && "yes".equals(paymentResponse.getBody())) {
            String orderPreCommitUrl = "http://localhost:8081/order/preCommit?orderId=" + orderId;
            String paymentPreCommitUrl = "http://localhost:8082/payment/preCommit?paymentId=" + paymentId;

            orderResponse = restTemplate.postForEntity(orderPreCommitUrl, null, String.class);
            paymentResponse = restTemplate.postForEntity(paymentPreCommitUrl, null, String.class);

            if ("ack".equals(orderResponse.getBody()) && "ack".equals(paymentResponse.getBody())) {
                restTemplate.postForEntity("http://localhost:8081/order/doCommit?orderId=" + orderId, null, String.class);
                restTemplate.postForEntity("http://localhost:8082/payment/doCommit?paymentId=" + paymentId, null, String.class);
                return "Transaction committed";
            } else {
                restTemplate.postForEntity("http://localhost:8081/order/abort?orderId=" + orderId, null, String.class);
                restTemplate.postForEntity("http://localhost:8082/payment/abort?paymentId=" + paymentId, null, String.class);
                return "Transaction aborted";
            }
        } else {
            restTemplate.postForEntity("http://localhost:8081/order/abort?orderId=" + orderId, null, String.class);
            restTemplate.postForEntity("http://localhost:8082/payment/abort?paymentId=" + paymentId, null, String.class);
            return "Transaction aborted";
        }
    }
}

Java

Copy

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();
    }
}

Java

Copy

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.

Up Next
    Ebook Download
    View all
    Learn
    View all