Add Distributed Transactions

In modern microservices architectures, monolithic applications are frequently broken down into independent services, each maintaining its own dedicated database. While this architectural evolution brings exceptional scalability and flexibility, it also introduces a formidable challenge: maintaining data consistency across multiple service boundaries. This article explores the core concepts of distributed transactions and demonstrates how to effectively integrate them into real-world projects.

In a traditional monolithic system, we rely heavily on the ACID properties (Atomicity, Consistency, Isolation, Durability) provided by relational databases to ensure transaction safety. However, in a distributed microservices environment, a single business operation might require coordinated calls to Service A, Service B, and Service C—each backed by an entirely separate database instance.

The fundamental problem that distributed transactions aim to solve is quite straightforward: When multiple database nodes collaborate to fulfill a single business transaction, the system must guarantee that either all operations succeed completely or all of them roll back. Under no circumstances should a "partially successful, partially failed" inconsistent state be permitted.
Before incorporating distributed transactions into your system, it is vital to understand the primary architectural patterns available so you can choose the right tool for your specific business scenario:

  1. 2PC (Two-Phase Commit)

    • Mechanism: Divided into a prepare phase and a commit phase, managed centrally by a coordinator that oversees the status of all participants.
    • Pros & Cons: Delivers strong consistency, but suffers from synchronous blocking, performance bottlenecks, and a single point of failure.
  2. TCC (Try-Confirm-Cancel)

    • Mechanism: Imposes high business invasiveness, requiring developers to implement three distinct interfaces (Try, Confirm, and Cancel) for every operation.
    • Pros & Cons: Offers solid runtime performance, but demands significantly higher development and maintenance costs.
  3. Reliable Event Sourcing / Eventual Consistency (Asynchronous Messaging)

    • Mechanism: Utilizes transactional messaging features from message brokers (such as RocketMQ or RabbitMQ) to ensure that message delivery and local transactions succeed simultaneously, eventually achieving data synchronization.
    • Pros & Cons: Provides excellent decoupling and high throughput, though it only guarantees eventual consistency and cannot satisfy strict real-time requirements.
  4. Sagas Pattern (Compensating Local Transactions)

    • Mechanism: Comprises a sequence of local transactions where every operation is paired with a corresponding compensating action. If step $N$ fails, the system sequentially executes compensation logic for steps $1$ through $N-1$.

Hands-On: Implementing Distributed Transactions with Seata

Seata (Simple Extensible Autonomous Transaction Architecture) is an open-source distributed transaction middleware created by Alibaba that offers high-performance and user-friendly transaction services. Below, we walk through setting up Seata's AT (Auto Transaction) mode within a microservices application.

Step 1: Add Dependencies

Include the Seata client starter dependency in your Maven project configuration:

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.6.1</version>
</dependency>

Step 2: Configure Global Transactions

In your application configuration file (application.yml), specify the essential Seata properties, including the application identifier and transaction service group:

seata:
  enabled: true
  application-id: order-service
  tx-service-group: my_test_tx_group
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848

Step 3: Database Preparation (AT Mode)

Seata's AT mode relies on an undo_log table within each participating business database to keep track of rollback logs. You need to execute the following DDL statement on every database involved in the distributed transaction:

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

Step 4: Apply the @GlobalTransactional Annotation

Annotate the entry-point method of your business logic (typically situated inside the order service) with @GlobalTransactional. Seata will automatically intercept this method invocation and initiate a global transaction context.

import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderServiceImpl implements OrderService {

    private final StockClient stockClient;
    private final OrderMapper orderMapper;

    public OrderServiceImpl(StockClient stockClient, OrderMapper orderMapper) {
        this.stockClient = stockClient;
        this.orderMapper = orderMapper;
    }

    @Override
    @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
    public void createOrder(Order order) {
        // 1. Create the order locally
        orderMapper.insert(order);

        // 2. Invoke the remote stock service to deduct inventory
        stockClient.decreaseStock(order.getProductId(), order.getCount());

        // 3. Simulate an exception: triggering a global rollback if validation fails
        if (order.getTotalAmount() == null || order.getTotalAmount().doubleValue() < 0) {
            throw new RuntimeException("Invalid order amount, triggering rollback");
        }
    }
}

Conclusion

Incorporating distributed transactions into a microservices ecosystem is a critical milestone for safeguarding data integrity. In practical software engineering, there is no silver-bullet solution that fits every use case. Developers must carefully evaluate their business requirements regarding consistency models, performance throughput, and maintenance complexity when choosing among 2PC, TCC, reliable messaging, or frameworks like Seata. Above all, keep in mind that distributed transactions inherently introduce performance overhead; systems should ideally be designed with thoughtful domain boundaries to minimize unnecessary cross-database transactions whenever possible.