Skip to content

How to Set Up Apache Seata for Distributed Transactions in Spring Boot

I deployed my first microservices architecture and immediately hit a wall: my distributed transactions were failing. I had two services—a payment service and an inventory service—each with its own database. When a user placed an order, I needed to deduct inventory and process payment atomically. Traditional Spring @Transactional only worked within a single database, leaving me with inconsistent data across services.

Error Message
org.springframework.dao.CannotAcquireLockException:
Unable to acquire lock on inventory item. Payment processed but inventory not updated.

This is the classic distributed transaction problem. After researching solutions like XA transactions, Saga pattern, and TCC (Try-Confirm-Cancel), I chose Apache Seata for its simplicity and AT (Automatic Transaction) mode.

What is Apache Seata?

Apache Seata is an open-source distributed transaction solution. It provides a Transaction Coordinator (TC) that manages transactions across multiple microservices, ensuring ACID properties across distributed databases.

The architecture has three components:

  1. Transaction Coordinator (TC): Maintains state of global transactions
  2. Transaction Manager (TM): Initiates and coordinates global transactions (your service)
  3. Resource Manager (RM): Manages branch transactions on each database

Step 1: Run Seata Server with Docker

First, I needed the Seata Server running. I tried the standalone Docker approach:

docker-compose.yml
services:
seata-server:
image: apache/seata-server:2.6.0
container_name: seata-server
ports:
- "7091:7091"
- "8091:8091"
environment:
- SEATA_IP=0.0.0.0
volumes:
- ./seata-config:/seata-server/resources
Output
[Server] seata-server started service port: 8091
[Server] seata-server started registry port: 7091

The server exposes two ports:

  • 8091: Transaction service port (clients connect here)
  • 7091: Registry port (for service discovery)

I initially made a mistake by only exposing port 7091, and my Spring Boot app couldn’t connect:

Error Message
io.seata.common.exception.FrameworkException: can not connect to services-server

Exposing both ports fixed the connection issue.

Step 2: Add Maven Dependencies

In my Spring Boot project, I added the Seata starter:

pom.xml
<dependency>
<groupId>org.apache.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.6.0</version>
</dependency>

The starter provides auto-configuration, so I didn’t need to manually configure beans. Make sure the version matches your Seata Server version—mismatched versions caused serialization errors in my testing.

Step 3: Configure Seata Connection

I created a seata.conf file in my resources directory:

seata.conf
transport {
type = "TCP"
server = "NIO"
heartbeat = true
enableClientBatchSendRequest = true
}
service {
vgroupMapping.my_tx_group = "default"
default.grouplist = "127.0.0.1:8091"
enableDegrade = false
disableGlobalTransaction = false
}
config {
type = "file"
}

The vgroupMapping maps my transaction group to a Seata cluster. For production, you’d use Nacos or Apollo for configuration, but file-based configuration works for development.

Then I added Seata properties to application.properties:

application.properties
seata.enabled=true
seata.application-id=payment-service
seata.tx-service-group=my_tx_group
seata.service.vgroup-mapping.my_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091
seata.config.type=file
seata.registry.type=file

The tx-service-group identifies which transaction group this service belongs to. Multiple services in the same transaction group will participate in the same distributed transaction.

Step 4: Create the undo_log Table

Seata’s AT mode requires an undo_log table in each database. This table stores before-images of data changes, allowing automatic rollback if the transaction fails.

For PostgreSQL, I created:

undo_log.sql
CREATE TABLE IF NOT EXISTS undo_log (
id BIGSERIAL NOT NULL,
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info BYTEA NOT NULL,
log_status INT NOT NULL,
log_created TIMESTAMP(0) NOT NULL,
log_modified TIMESTAMP(0) NOT NULL,
CONSTRAINT pk_undo_log PRIMARY KEY (id),
CONSTRAINT ux_undo_log UNIQUE (xid, branch_id)
);
CREATE INDEX idx_log_created ON undo_log(log_created);

For MySQL, the table is similar but uses LONGBLOB instead of BYTEA:

undo_log-mysql.sql
CREATE TABLE IF NOT EXISTS undo_log (
id BIGINT NOT NULL AUTO_INCREMENT,
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT NOT NULL,
log_created DATETIME(6) NOT NULL,
log_modified DATETIME(6) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB;

I forgot this table initially and got:

Error Message
Table 'mydb.undo_log' doesn't exist

Seata needs this table to record snapshots before each data modification. If a transaction fails, Seata uses these snapshots to generate and execute SQL for rollback.

Step 5: Use @GlobalTransactional

Now I could use distributed transactions in my service:

OrderService.java
@Service
public class OrderService {
@Autowired
private PaymentServiceClient paymentClient;
@Autowired
private InventoryService inventoryService;
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public Order createOrder(OrderRequest request) {
// Step 1: Reserve inventory
inventoryService.reserveStock(request.getProductId(), request.getQuantity());
// Step 2: Process payment
paymentClient.processPayment(request.getPaymentInfo());
// Step 3: Create order record
return orderRepository.save(new Order(request));
}
}

The @GlobalTransactional annotation marks the transaction boundary. All database operations within this method—and in any services called by this method—will participate in the global transaction.

If any operation fails, Seata automatically rolls back all changes across all databases:

Output
[Seata] Begin new global transaction [192.168.1.100:8091:1234567890]
[Seata] Branch transaction registered: payment-service:1234567891
[Seata] Branch transaction registered: inventory-service:1234567892
[Seata] Global transaction commit: 1234567890

How AT Mode Works

Understanding the mechanism helped me debug issues:

  1. Parse SQL: Seata intercepts SQL execution, parses the statement to understand what data will be modified
  2. Query Before-Image: Before executing the SQL, Seata queries the affected records
  3. Execute SQL: The actual SQL runs on the database
  4. Query After-Image: Seata queries the modified records
  5. Record Undo Log: Seata writes a rollback record to undo_log containing the before-image
  6. Register Branch: The RM registers a branch transaction with the TC
  7. Commit or Rollback: TC coordinates commit (deletes undo log) or rollback (uses undo log to generate reverse SQL)

This is why AT mode requires primary keys on tables—Seata needs to uniquely identify rows for the before/after image queries.

Common Pitfalls I Encountered

Primary Key Required

Tables without primary keys won’t work with AT mode:

Error Message
Primary key not found in table: audit_log

For such tables, consider using Seata’s XA mode or manually managing transactions.

Connection Pool Compatibility

Not all connection pools work with Seata. I had issues with HikariCP in some configurations. The recommended approach is:

application.properties
spring.datasource.driver-class-name=io.seata.rm.datasource.DataSourceProxy

Or configure it programmatically:

DataSourceConfig.java
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DataSourceProxy(new HikariDataSource());
}
}

Network Latency

In production, the two-phase commit adds latency. Each transaction requires multiple round-trips to the TC:

  1. Begin global transaction
  2. Register each branch transaction
  3. Global commit or rollback

For high-throughput scenarios, consider:

  • Saga mode: Better for long-running transactions
  • TCC mode: More control but requires more code
  • XA mode: Standard but heavier resource usage

Monitoring and Debugging

Seata provides a console at http://localhost:7091. You can see active transactions, branch transactions, and their status.

For production, integrate with Prometheus and Grafana. Seata exposes metrics:

application.yml
seata:
metrics:
enabled: true
registry-type: compact
exporter-list: prometheus

Key Takeaways

  1. Seata Server is the coordinator: It must be running and accessible
  2. AT mode is easiest: Requires only the undo_log table
  3. Primary keys are mandatory: Seata needs them for snapshot queries
  4. Version matching matters: Client and server versions should match
  5. Test rollback scenarios: Ensure your undo_log works correctly

Distributed transactions are complex, but Seata’s AT mode makes them manageable. The key is understanding the two-phase commit and ensuring proper configuration across all participating services.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!


Comments