Seata the deal: no more distributed transaction nightmares across (Spring Boot) microservices

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 microservices pattern 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 how Apache 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:
Transaction Coordinator (TC): maintain status of global and branch transactions, drive the global commit or rollback.
Transaction Manager (TM): define the scope of global transaction: begin a global transaction, commit or rollback a global transaction.
Resource Manager (RM): manage resources that branch transactions working on, talk to TC for registering branch transactions and reporting status of branch transactions, and drive the branch transaction commit or rollback.
The supported transaction models are: AT, XA (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).
- Phase 1: Prepare (Commit Vote):
TC (Seata Server), asks all participating RMs (e.g., your microservices interacting with databases) if they are ready to commit their local transaction.
Each RM performs its operations, writes transaction logs to ensure durability, and locks resources to guarantee isolation.
If an RM is ready, it responds “Yes” to the TC. If not, it responds “No”.
2. Phase 2: Commit or Rollback:
If all RMs respond “Yes”, the TC sends a “Commit” command to all RMs. They finalize their local transactions and release locks.
If any RM responds “No”, or if the TC detects a timeout/failure, the TC sends a “Rollback” command. All RMs undo their operations and release locks.
Pros & Cons (Briefly):
Pro: Provides strong data consistency (ACID) across multiple services, acting like a single, unbreakable transaction.
Con: Can lead to resource blocking (high latency) if participants or the coordinator are slow or fail, potentially impacting availability and scalability. It also relies on underlying databases/resources supporting the XA standard.
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.
No Global Locks: crucially, local transactions commit immediately and do not hold global locks, allowing for greater concurrency.
Compensation for Failure: if any local transaction fails, the Saga does not “rollback” in the traditional sense. Instead, it executes a series of compensating transactions to semantically undo the effects of previously completed local transactions. These compensating transactions are new operations designed to reverse the business impact.
Saga can be implemented via:
Choreography: Services publish events and react to them, leading to a decentralized flow.
Orchestration: A central orchestrator service coordinates the flow, sending commands and reacting to responses.
Pros & Cons (Briefly):
Pro: Excellent for high availability and scalability due to lack of long-held distributed locks. Ideal for loosely coupled microservices.
Con: Achieves eventual consistency, meaning data might be temporarily inconsistent. Requires significant development effort to implement all compensating transactions and manage complex Saga logic, which can also make debugging harder.
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.
- Phase 1: Local Transaction & Prepare:
When a microservice (RM) performs a database operation (e.g., UPDATE, INSERT, DELETE) within a global transaction:
Seata’s intelligent DataSourceProxy intercepts the SQL.
It automatically creates an undo_log (recording the data’s state before the modification).
The SQL operation is executed and committed immediately on the local database.
Seata then acquires a global lock for the modified resource via the Transaction Coordinator (TC). This lock is not a traditional database lock; it prevents other global transactions from concurrently modifying the same resource, but doesn’t block read operations.
The RM informs the TC that its branch transaction is “prepared.”
2. Phase 2: Global Commit or Rollback:
Global Commit: If all branch transactions prepare successfully, the TC instructs them to commit. Since local DB transactions were already committed in Phase 1, RMs simply release their global locks.
Global Rollback: If any branch transaction fails, or the global transaction needs to rollback:
The TC instructs the RMs to roll back.
RMs use their stored undo_log to automatically compensate for the changes made to their local databases, effectively restoring the previous state. They then release their global locks.
Pros & Cons (Briefly):
Pro: Provides strong consistency for the global transaction. Offers excellent availability and scalability as local database locks are held only briefly. It’s highly transparent to developers, requiring minimal code changes. Automatic rollback simplifies error handling.
Con: Primarily designed for relational databases. While non-blocking at the DB level, there’s still an overhead from generating undo_logs and managing global locks.
A practical demonstration with Spring Boot
Let’s envision a scenario involving two distinct microservices, each operating with its own dedicated and autonomous database:
Credit API: responsible for managing user credit (money balance)
Shipping API: dedicated to handling shipment purchases
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:
Checking and/or deducting user credit via the Credit API.
Actually purchasing the shipment using the credit, via the Shipping API.
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:
Registry: defines the Service Registry that Seata will use (nacos, eureka, consul, zookeper, redis, file). For this example I chose to use the file type
Store: defines the persistence of global transaction data and global locks (file, db, redis). For this example I chose to use db type.
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
Atomikos: XA (2PC). It only works within the same microservice that accesses multiple databases (there is no Transaction Coordinator, no XID propagation)
Workflow engine for SAGA orchestration (Camunda, Temporal.io): requires external orchestrator and higher integration effort.
SAGA (choreography event-driven): eventual consistency trade-off.
Outbox pattern: eventual consistency.
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.
Subscribe to my newsletter
Read articles from Biagio Tozzi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Biagio Tozzi
Biagio Tozzi
404 - Biography Not Found