In a time when nostalgia may have gone a little too far, the software development world has been swept by a heated debate: has the massive adoption of the microservicespattern truly delivered the expected benefits, or is its balance sheet more uncertain? Many teams are starting to wonder whether it’s time for a “homecoming” to the good old, reliable monolith — or if the answer lies somewhere in between, in the form of the modular monolith.
Excluding the cost factor, this rethinking often stems from the inherent complexity of distributed systems, particularly one of their crucial pain points:distributed transactions.
This article aims to address precisely this topic, demonstrating howApache Seata, in this case combined with the agility of Spring Boot, manages to transform what for many is a nightmare into a surprisingly manageable solution. I will show you how Seata can help you sleep soundly at night, eliminating the need for complex architectures and rollback strategies and/or resorting to more cumbersome patterns like the Outbox pattern.

Apache Seata

Apache Seata is an open source distributed transaction solution, currently in Apache incubation, that delivers high performance and easy to use distributed transaction services under a microservices architecture.

The main components are:

The supported transaction models are: ATXA (2PC) and SAGA.

(If you’d rather skip the theory, go straight to the practical solution!)

XA (2PC)

XA is a specification released in 1991 by X/Open (which later merged with The Open Group).

  1. Phase 1: Prepare (Commit Vote):

2. Phase 2: Commit or Rollback:

Pros & Cons (Briefly):

SAGA

The SAGA pattern is a widely adopted approach in microservices architectures to manage distributed transactions. Unlike 2PC, Saga sacrifices immediate strong consistency for higher availability and scalability, achieving eventual consistency.

A Saga is a sequence of local transactions, where each local transaction (within a single microservice) updates its own database and then publishes an event or message to trigger the next local transaction in the sequence.

Saga can be implemented via:

Pros & Cons (Briefly):

AT

The AT (Automatic Transaction) Mode is Seata’s flagship solution, aiming to offer the ease-of-use of 2PC with the non-blocking nature and scalability benefits of Saga. It’s the recommended default for most microservices using relational databases.

  1. Phase 1: Local Transaction & Prepare:

2. Phase 2: Global Commit or Rollback:

Pros & Cons (Briefly):

A practical demonstration with Spring Boot

Let’s envision a scenario involving two distinct microservices, each operating with its own dedicated and autonomous database:

Orchestrating these two services is a BFF (Backend For Frontend). Its role is to coordinate the shipment purchase operation, which translates into a sequence of distributed calls:

The crucial question then arises: how can we ensure that these operations, distributed across different services and databases, maintain their transactional consistency, guaranteeing that the purchase is completed only if the credit has been successfully updated, and vice-versa?

Architecture


TM

The BFF will represent the Transaction Manager, i.e. it will define the global transaction.

@Service
public class BFFManager {

    @Autowired
    private CreditService creditService;

    @Autowired
    private ShippingService shippingService;

    @GlobalTransactional
    public ShippingResult buyShipping(Long userID, BigDecimal cost) {
        var wallet = creditService.updateBalance(userID, cost);
        var shipping = shippingService.buyShipping(userID, cost);
        
        wallet = creditService.getWallet(userID);

        var result = new ShippingResult();
        result.setCost(cost);
        result.setShippingID(shipping.getId());
        result.setCurrentBalance(wallet.getBalance());
        return result;
    }
}

So just the @GlobalTransactional annotation is enough? Of course not, but little else is needed:

Dependencies needed for TM:

implementation 'org.apache.seata:seata-spring-boot-starter:2.3.0'
implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-seata:2023.0.3.3') {
  exclude group: 'org.apache.seata', module: 'seata-spring-boot-starter'
}

spring-cloud-starter-alibaba-seata, among other things it can do, ensures that http communications between microservices always contain the Global Transaction ID (XID).

seata-spring-boot-starter is the classic Spring Boot starter that autoconfigures the Seata entity (in this case the TM) starting from the properties:

seata:
  enabled: true
  data-source-proxy-mode: AT
  enable-auto-data-source-proxy: true
  application-id:  ${spring.application.name}
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 127.0.0.1:8091

RM

credit-api and shipping-api act as RM. They only need dependency seata-spring-boot-starter with following properties:

seata:
  enabled: true
  data-source-proxy-mode: AT
  enable-auto-data-source-proxy: true
  application-id:  ${spring.application.name}
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 127.0.0.1:8091

It is necessary that the RM DB contains the undo_log table for Seata. Here are the necessary scripts for each type of DB.

In the code that you will find on GitHub the creation of the table is managed through the docker compose that creates the dedicated db (through org.springframework.boot:spring-boot-docker-compose)

In RMs, no specific annotation is needed. Write your code towards the repository as you always have. If you want, and I recommend it, continue to use @Transactional for local transactions.

TC

The TC is represented by the heart of Seata, seata-server. This requires two main configurations:

Here’s the docker compose setup used to initialize the server:

services:
  mysql:
    image: mysql:8.0.33
    container_name: mysql-seata
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: seata
      MYSQL_USER: seata_user
      MYSQL_PASSWORD: seata_pass
    ports:
      - "3317:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./docker/seata/mysql.sql:/docker-entrypoint-initdb.d/seata.sql:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-prootpass"]
      interval: 10s
      timeout: 5s
      retries: 5

  seata-server:
    image: apache/seata-server:2.3.0
    container_name: seata-server
    depends_on:
      mysql:
        condition: service_healthy
    environment:
      - SEATA_CONFIG_NAME=application.yml
    volumes:
      - "./docker/seata/resources/application.yml:/seata-server/resources/application.yml"
      - "./docker/seata/mysql-connector-j-8.0.33.jar:/seata-server/libs/mysql-connector-j-8.0.33.jar"
    ports:
      - "7091:7091"
      - "8091:8091"

volumes:
  mysql_data:

And the configuration properties (application.yaml):

server:
  port: 7091

spring:
  application:
    name: seata-server
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://mysql:3306/seata
    username: seata_user
    password: seata_pass

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${log.home:${user.home}/logs/seata}

console:
  user:
    username: seata
    password: seata

seata:
  security:
    secretKey: seata
    tokenValidityInMilliseconds: 1800000

  config:
    type: file

  registry:
    type: file

  store:
    mode: db
    db:
      dbType: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://mysql:3306/seata
      user: seata_user
      password: seata_pass
      min-conn: 10
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      vgroup-table: vgroup_table
      query-limit: 1000
      max-wait: 5000

As you may have noticed, the dedicated database needs a number of tables to function. All the creation sql scripts are available here.

Play!

@SpringBootTest
public class GlobalTransactionalTest {

    @Autowired
    private BFFManager bffManager;

    @Autowired
    private CreditService creditService;

    @MockitoBean
    private ShippingService shippingService;

    @Test
    public void globalTransactionalTest_OK() {
        var wallet = creditService.getWallet(1L);
        var shipping = new Shipping();
        shipping.setId(2L);
        when(shippingService.buyShipping(1L, new BigDecimal(4))).thenReturn(shipping);
        bffManager.buyShipping(1L, new BigDecimal(4));
        var newWallet = creditService.getWallet(1L);
        assertEquals(new BigDecimal("4.00"), wallet.getBalance().subtract(newWallet.getBalance()));
    }

    @Test
    public void globalTransactionalTest_KO() {
        var wallet = creditService.getWallet(1L);
        var shipping = new Shipping();
        shipping.setId(2L);
        when(shippingService.buyShipping(1L, new BigDecimal(4))).thenThrow(new RuntimeException());

        try {
            bffManager.buyShipping(1L, new BigDecimal(4));
        } catch (Exception e) {}

        var newWallet = creditService.getWallet(1L);
        assertEquals(newWallet.getBalance(), wallet.getBalance());
    }

}

The complete and working code is available on GitHub. Run the components and let me know what you think!

Key alternatives

This article does not aim to promote Apache Seata over the other alternatives mentioned, but rather to highlight its ease of use. As always, the right tool should be chosen based on the specific context and system requirements.

Next episode: “How cool were the monoliths?” Stay tuned.